前言

不知道各位是否现已开端了解 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

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

所以在这个匿名函数中能直接拜访或修正 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 个函数的比照图和表格:

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

函数 上下文方式 回来值 是否是扩展函数
with 隐式接纳者(this) lambda函数(Unit)
run 隐式接纳者(this) lambda函数(Unit)
let 匿名函数的参数(it) lambda函数(Unit)
also 匿名函数的参数(it) 上下文目标
apply 隐式接纳者(this) 上下文目标

Compose 中的效果域约束

在前言中咱们说过,在 Compose 对效果域约束的运用十分多。

例如 Modifier 修饰符,从这个 Compose 修饰符列表 中,咱们也能看到许多修饰符的效果域都做了约束:

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

这儿需求对修饰符做约束的原因十分简略:

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 中还有许多效果域约束的比如,例如:

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

在上图中 item 只能在 LazyListScope 效果域运用,drawRect 只能在 DrawScope 效果域运用。

当然,正如咱们前面说的,效果域中不只要函数和办法,还能够拜访类的特点,例如,在 DrawScope 效果域供给了一个名为 size 的特点,能够经过它来拿到当前的画布大小:

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

那么,这些是怎样完成的呢?

自界说咱们的效果域约束函数

原理

在开端完成咱们自己的效果域函数之前,咱们需求先了解一下原理。

这儿咱们以 Compose 的 Canvas 为例来看看。

首先是 Canvas 的界说:

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

能够看到这儿 Canvas 接纳了两个参数:modifier 和 onDraw 的 lambda ,且这个 lambda 的 Receiver(接纳者) 为 DrawScope ,也便是说,onDraw 这个匿名函数的效果域被约束在了 DrawScope 内,这也意味着能够在匿名函数内部运用 DrawScope 效果域内的特点、办法等。

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

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

能够看到这是一个接口,里面界说了一些特点变量(如咱们上面说的 size) 和一些办法(如咱们上面说的 drawRect )。

然后再完成这个接口,编写具体完成代码:

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

完成

所以总结来说,假设咱们想完成自己的效果域约束大致分为三步:

  1. 编写作为效果域的接口
  2. 完成这个接口
  3. 在露出的办法中将 lambda 参数接纳者运用上面界说的接口

下面咱们举个比如。

假设咱们要在 Compose 中完成一个遮罩引导层,用于引导新用户操作,类似这样:

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

图源 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 效果域内,在外面是不能调用的:

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

总结

本文简要介绍了 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

本文正在参与「金石方案 . 分割6万现金大奖」