为 Kotlin 的函数添加作用域限制(以 Compose 为例)

news2024/11/25 22:32:34

前言

不知道各位是否已经开始了解 Jetpack Compose?

如果已经开始了解并且上手写过。那么,不知道你们有没有发现,在 Compose 中对于作用域(Scopes)的应用特别多。比如, weight 修饰符只能用在 RowScope 或者 ColumnScope 作用域中。又比如,item 组件只能用在 LazyListScope 作用域中。

如果你还没有了解过 Compose 的话,那你也应该知道,kotlin 标准库中有 5 个作用域函数:let() apply() also() with() run() ,这 5 个函数会以不同的方式持有和返回上下文对象,即调用这些函数时,在它们的 lambda 参数中写的代码将处于特定的作用域。

不知道你们有没有思考过,这些作用域限制是怎么实现的呢?如果我们想自定义一个 Composable 函数,只支持在特定的作用域中使用,应该怎么写呢?

本文将为你解开这个疑惑。

作用域

不过在正式开始之前我们还是先大概补充一点有关 kotlin 中作用域的基本知识。

什么是作用域

其实对于咱们程序员来说,不管学的是什么语言,对于作用域应该都是有一个了解的。

举个简单的例子:

val valueFile = "file"

fun a() {
    val valueA = "a"
    println(valueFile)
    println(valueA)
    println(valueB)
}

fun b() {
    val valueB = "b"
    println(valueFile)
    println(valueA)
    println(valueB)
}

这段代码不用运行都知道肯定会报错,因为在函数 a 中无法访问 valueB ;在函数 b 中无法访问 valueA 。但是这两个函数都可以成功访问 valueFile

这是因为 valueFile 的作用域是整个 .kt 文件,也就是说,只要是在这个文件中的代码,都可以访问到它。

valueAvalueB 的作用域则分别是在函数 a 和 b 中,显然只能在各自的作用域中使用。

同理,如果我们想要调用类的方法或者函数也需要考虑作用域:

class Test {
    val valueTest = "test"

    fun a(): String {
        val valueA = "a"
        println(valueTest)
        println(valueA)

        return "returnA"
    }
    
    fun b() {
       println(valueA)
       println(valueTest)
       println(a())
    }
}

fun main() {
    println(valueTest)
    println(valueA)
    println(a())
}

这里举的例子可能不太恰当,但是这里是为了说明这个情况,不要过多纠结哦~

显然,上面这个代码,在 main 函数中是无法访问到变量 valueTestvalueA 的,并且也无法调用函数 a() ;而在 Test 类中的函数 a() 显然可以访问到 valueTestvalueA ,并且函数 b() 也可以调用函数 a(),可以访问变量 valueTest 但是无法访问变量 valueA

这是因为函数 a()b() 以及变量 valueTest 位于同一个作用域中,即类 Test 的作用域。

而变量 valueA 位于函数 a() 的作用域内,由于 a() 又位于 Test 的作用域内,所以实际上这里的 valueA 的作用域称为嵌套作用域,即同时位于 a()Test 的作用域内。

因为本节只是为了引出我们今天要介绍的内容,所以有关作用域的知识就简单介绍这么多,更多有关作用域的知识可以阅读参考资料 1 。

kotlin 标准库中的作用域函数

在前言中我们说过,kotlin标准库中有5个称之为作用域函数的东西:withrunletalsoapply

它们有什么作用呢?

先看一段我们经常会遇到的代码形式:

val person = Person()
person.fullName = "equationl"
person.lastName = "l"
person.firstName = "equation"
person.age = 24
person.gender = "man"

在某些情况下,我们可能会需要多次重复的写一堆 person,可读性很差,写起来也很繁琐。

此时我们就可以使用作用域函数,例如使用 with 改写:

with(person) {
    fullName = "equationl"
    lastName = "l"
    firstName = "equation"
    age = 24
    gender = "man"
}

此时,我们就可以省略掉 person ,直接访问或修改它的属性值,这是因为 with 的第一个参数接收的是需要作为第二个参数的 lambda 上下文对象,即此时,第二个参数 lambda 匿名函数所在的作用域为第一个参数传入的对象,此时 IDE 的提示也指出了此时 with 的匿名函数中的作用域为 Person

1.png

所以在这个匿名函数中能直接访问或修改 Person 的属性。

同理,我们也可以使用 run 函数改写:

person.run {
    fullName = "equationl"
    lastName = "l"
    firstName = "equation"
    age = 24
    gender = "man"
}

可以看出,runwith 非常相似,只是 run 是以扩展函数的形式接收上下文对象,它的参数只有一个 lambda 匿名函数。

后面还有 let

person.let {
    it.fullName = "equationl"
    it.lastName = "l"
    it.firstName = "equation"
    it.age = 24
    it.gender = "man"
}

它与 run 的区别在于,匿名函数中的上下文对象不再是隐式接收器(this),而是作为一个参数(it)存在。

使用 also() 则是:

person.also {
    it.fullName = "equationl"
    it.lastName = "l"
    it.firstName = "equation"
    it.age = 24
    it.gender = "man"
}

let 一样,它也是扩展函数,并且上下文也作为参数传入匿名函数,但是不同于 let ,它会返回上下文对象,这样可以方便的进行链式调用,如:

val personString = person
    .also {
        it.age = 25
    }
    .toString()

最后是 apply

person.apply {
    fullName = "equationl"
    lastName = "l"
    firstName = "equation"
    age = 24
    gender = "man"
}

also 一样,它是扩展函数,也会返回上下文对象,但是它的上下文将作为隐式接收者,而不是匿名函数的一个参数。

下面是它们 5 个函数的对比图和表格:

2.png

函数上下文形式返回值是否是扩展函数
with隐式接收者(this)lambda函数(Unit)
run隐式接收者(this)lambda函数(Unit)
let匿名函数的参数(it)lambda函数(Unit)
also匿名函数的参数(it)上下文对象
apply隐式接收者(this)上下文对象

Compose 中的作用域限制

在前言中我们说过,在 Compose 对作用域限制的应用非常多。

例如 Modifier 修饰符,从这个 Compose 修饰符列表 中,我们也能看到很多修饰符的作用域都做了限制:

3.png

这里需要对修饰符做限制的原因非常简单:

In the Android View system, there is no type safety. Developers usually find themselves trying out different layout params to discover which ones are considered and their meaning in the context of a particular parent.

在传统的 xml view 体系中就是没有对布局的参数做限制,这就导致所有的参数都可以用在任意布局中,这会导致一些问题。轻则参数无效,写了一堆无用参数;严重的可能会干扰到布局的正常使用。

当然,Modifier 修饰符限制只是 Compose 中其中一个应用,在 Compose 中还有很多作用域限制的例子,例如:

4.png

在上图中 item 只能在 LazyListScope 作用域使用,drawRect 只能在 DrawScope 作用域使用。

当然,正如我们前面说的,作用域中不只有函数和方法,还可以访问类的属性,例如,在 DrawScope 作用域提供了一个名为 size 的属性,可以通过它来拿到当前的画布大小:

5.png

那么,这些是怎么实现的呢?

自定义我们的作用域限制函数

原理

在开始实现我们自己的作用域函数之前,我们需要先了解一下原理。

这里我们以 Compose 的 Canvas 为例来看看。

首先是 Canvas 的定义:

6.png

可以看到这里 Canvas 接收了两个参数:modifier 和 onDraw 的 lambda ,且这个 lambda 的 Receiver(接收者) 为 DrawScope ,也就是说,onDraw 这个匿名函数的作用域被限制在了 DrawScope 内,这也意味着可以在匿名函数内部使用 DrawScope 作用域内的属性、方法等。

再来看看这个 DrawScope 是何方神圣:

7.png

可以看到这是一个接口,里面定义了一些属性变量(如我们上面说的 size) 和一些方法(如我们上面说的 drawRect )。

然后再实现这个接口,编写具体实现代码:

8.png

实现

所以总结来说,如果我们想实现自己的作用域限制大致分为三步:

  1. 编写作为作用域的接口
  2. 实现这个接口
  3. 在暴露的方法中将 lambda 参数接收者使用上面定义的接口

下面我们举个例子。

假如我们要在 Compose 中实现一个遮罩引导层,用于引导新用户操作,类似这样:

main_intro.gif

图源 Intro-showcase-view

但是我们希望引导层上的提示可以多样化,例如可以支持文字提示、图片提示、甚至播放视频或动图提示,但是我们不希望这些提示 item 在遮罩层以外的地方被调用,因为它们依赖于遮罩层的某些参数,如果在外部调用会出错。

这时候,使用作用域限制就非常合适。

首先,我们编写一个接口:

interface ShowcaseScreenScope {
    val isShowOnce: Boolean

    @Composable
    fun ShowcaseTextItem()
}

在这个接口中我们定义了一个属性变量 isShowOnce 用于表示这个引导层是否只显示一次、定义一个方法 ShowcaseTextItem 表示在引导层上显示一串文字,同理我们还可以定义 ShowcaseImageItem 表示显示图片。

然后实现这个接口:

private class ShowcaseScopeImpl: ShowcaseScreenScope {

    override val isShowOnce: Boolean
        get() = TODO("在这里编写是否只显示一次的逻辑")

    @Composable
    override fun ShowcaseTextItem() {
        // 在这里写你的实现代码
        Text(text = "我是说明文字")
    }
}

在接口实现中,根据我们的需求编写相应的实现逻辑代码。

最后,写一个提供给外部调用的 Composable:

@Composable
fun ShowcaseScreen(content: @Composable ShowcaseScreenScope.() -> Unit) {
    // 在这里实现其他逻辑(例如显示遮罩)后调用 content
    // ……
    ShowcaseScopeImpl().content()
}

在这个 composable 中,我们可以先处理完其他逻辑,例如显示遮罩层 UI 或显示动画后再调用 ShowcaseScopeImpl().content() 将我们传递的子 Item 组合上去。

最后,使用时只需要调用:

ShowcaseScreen {
    if (!isShowOnce) {
        ShowcaseTextItem()
    }
}

当然,这个 ShowcaseTextItem()isShowOnce 位于 ShowcaseScreenScope 作用域内,在外面是不能调用的:

9.png

总结

本文简要介绍了 Kotlin 中的作用域概念和标准库中的作用域函数,并引申到 Compsoe 中关于作用域的应用,最终分析实现原理并讲解如何自定义一个我们自己的 Compose 作用域函数。

本文写的可能比较浅显,很多知识点都是点到为止,没有过多讲解,推荐读者阅读完后,可以看看文末的参考链接中其他大佬写的文章。

参考资料

  1. Scopes and Scope Functions
  2. Kotlin DSL 实战:像 Compose 一样写代码
  3. Scope composables to a parent composable
  4. Compose modifiers-Type safety in Compose

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/574304.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

docker快速部署hue+hue集成hive

首先需要安装hive,hive的安装在HIVE的安装与配置_EEEurekaaa!的博客-CSDN博客 安装完成之后,使用脚本命令启动hdfs和hive的相关服务。 一、安装docker # 安装yum-config-manager配置工具 $ yum -y install yum-utils # 设置yum源 $ yum-co…

《俞军产品方法论》- 站在更高的角度来拓展产品经理的内涵和边界

关于作者 俞军,互联网产品大神级人物。他是早年百度唯一的产品经理,主持了百度搜索这款产品的无数次进化,并主持设计了百度贴吧、百度 知道等世界级创新产品,后来又成为滴滴出行的产品负责人。他的 “ 俞军产品经理十二条 ” &a…

TexSAW|2023|Cryptography&Misc|WP

Cryptography|Crack the crime 用 nc 连上后,直接得到第一题 是一个简单的base64加密,解密如下: Meet in dubai on Tuesday 填入之后可获得第二题 猜测是古典加密,随后经过N次尝试后发现是rot13加密,解密…

蓝牙耳机怎么选?蓝牙耳机哪些性价比高?2023年蓝牙耳机推荐,蓝牙耳机品牌排行榜,蓝牙耳机最全选购指南

蓝牙耳机怎么选?蓝牙耳机哪些性价比高?2023年蓝牙耳机推荐,蓝牙耳机品牌排行榜,蓝牙耳机最全选购指南 观前提醒,本文中你将会了解到: |蓝牙耳机抄作业环节 |蓝牙耳机基础知识 &a…

快速上手kettle

一、前言 最近由于工作需要,需要用到kettle工具进行数据迁移转换。特意找资料学习了一下,kettle基本操作算是学会了。 所学的也结合实际工作进行了验证。为了防止以后用到忘记了,便写了几篇文章记录一下。 二 、ETL简介 ETL ( Extract-Tran…

synchronized 关键字和 volatile 关键字有什么区别?

synchronized 关键字和 volatile 关键字有什么区别? 在 Java 中,synchronized 关键字和 volatile 关键字都可以用来实现线程安全,但是它们有不同的用途和实现方式。本文将介绍 synchronized 关键字和 volatile 关键字的区别,包括…

二肽二氨基丁酰苄基酰胺二乙酸盐/Dipeptide Diaminobutyroyl Benzylamide Diacetate/SYN-AKE

作用机理----二肽二氨基丁酰苄基酰胺二乙酸盐 类蛇毒三肽通过松弛面部肌肉而作为有效的平滑和祛皱活性产品, 该活性三肽作用方式与 Temple Viper 毒蛇毒液的神经肌肉阻断化合物Waglerin 1 一致。类蛇毒三肽作用于突触后膜, 是肌肉烟碱乙酰胆碱受体(nmAChR)可逆转的拮抗剂。类蛇…

docker安装单机nacos、rocketmq、reids、xxl-job、minio、elasticsearch、kibana

启动容器报错 直接删除那个name后边的就可以 安装nacos 首先需要拉取对应的镜像文件:docker pull nacos/nacos-server 挂载目录: mkdir -p /mydata/nacos/logs/ #新建logs目录mkdir -p /mydata/nacos/init.d/ vim /myda…

使用 Kotlin 的 Opt-in (选择加入)功能注解API提示当前非稳定API

前言 之前在给公司项目封装库的时候,领导告诉我封装的漂亮一点,等以后公司发展起来了可能需要把这个库提供给第三方接入使用。 此时,就有这么一个问题:某些功能函数使用条件比较苛刻,直接使用可能会出现意想不到的后…

Mock.js 的语法规范学习

Mock.js 有一套完整的语法规范,可以好好学学。 Mock.js 的语法规范包括两部分: 数据模板定义规范(Data Template Definition,DTD) 数据占位符定义规范(Data Placeholder Definition,DPD) 数…

【mediasoup】12: ChannelRequest控制指令

rust 是把worker 当做lib 调用的。node是当做一个进程每一个ChannelRequest 就是一个外部发给worker的控制指令worker要负责处理。控制指令的处理实际是worker做的,worker可能立即执行,可能交给对应的handler去处理 worker根据指令id 来处理 处理完毕后才发消息ack 给控制侧 …

# Spring Boot 中如何使用 Spring Cloud Sleuth 来实现分布式跟踪?

Spring Boot 中如何使用 Spring Cloud Sleuth 来实现分布式跟踪? 在微服务架构中,通常会有多个服务相互协作,为了方便排查问题,我们需要对服务之间的调用进行跟踪。Spring Cloud Sleuth 是 Spring Cloud 生态中的分布式跟踪解决方…

charles使用

charles​ 一、概念​ charles是一款非常优秀的抓包工具,全平台支持,在mac,windows,linux上都可以使用,既可以抓 取web端的包,也可以抓app端的包。 ​ charles主要的功能包括如下几点: ​ 截取…

Linux网络服务:SSH远程访问及控制2

目录 一、理论 1.构建密钥对验证的SSH体系 2.TCP Wrappers访问控制 二、实验 1.ecdsa免密连接 2.rsa免密连接 一、理论 1.构建密钥对验证的SSH体系 (1)免密连接原理 ① 手动添加客户端的公钥到服务端 ② 服务端收到客户端的公钥后使用客户端公钥…

C++——引用

引用的概念 初步理解:引用相当于给变量取了一个别名,它和引用的变量共用同一块空间。 就好比孙悟空有很多外号,例如孙行者,齐天大圣,斗战胜佛,但是它们所指都是孙悟空。同样的,如果齐天大圣大…

如何在 Ubuntu 22.04 上安装 Python Pip?

Python Pip 是 Python 的包管理器,它允许您轻松地安装和管理 Python 包和库。在 Ubuntu 22.04 上安装 Python Pip 是非常简单的。 本文将详细介绍如何在 Ubuntu 22.04 上安装 Python Pip,并为您提供逐步指南。 步骤 1:更新软件包列表 在安装…

C Primer Plus第八章编程练习答案

学完C语言之后,我就去阅读《C Primer Plus》这本经典的C语言书籍,对每一章的编程练习题都做了相关的解答,仅仅代表着我个人的解答思路,如有错误,请各位大佬帮忙点出! 1.设计一个程序,统计在读到…

Yum使用方法

1.什么是软件包 在Linux下安装软件,有三种方法: 通过对源代码进行封装,并进行编译,得到可执行程序。rpm安装,rpm安装软件需要各种指令,对于小白来说不友好,容易出错。yum安装,解决…

六、Docker仓库之Harbor搭建(三)

Harbor搭建 一、Harbor简介 1.Harbor介绍 Harbor是一个用于存储Docker镜像的企业级镜像服务器,通过添加一些企业必需的功能特性,如安全、标识和管理等,大大扩展其功能。作为一个企业级私有镜像服务器,Harbor提供了更好的性能和安…

Modern CSV:大型 CSV 文件编辑器/查看器 Crack

Modern CSV用于快速查看大型 CSV 文件 适用于 Windows、Mac 和 Linux 的复杂 CSV 编辑器/查看器 被使用 电子商务运营商。数据科学家。会计师。 IT 专业人员。学生。医学研究人员。数字营销人员。生物学家。工程师。 现代 CSV 是适用于 Windows、Mac 和 Linux 的功能强大的表格…