Jetpack Compose——interface Modifier

Modifier 接口

modifier: Modifier = Modifier

自定义 Composable 函数时,通常会将 modifier 参数作为第一个可选参数。这样,调用者就可以使用 Modifier 对象来修改组合内容的行为和外观。

kotlin 复制代码
@Composable 
fun CustomCard(
    modifier: Modifier = Modifier, 
    content: @Composable () -> Unit
) { ... }

modifier: Modifier = Modifier 里 3 个单词都一样,分别都是什么意思呢?

  • modifier 最左边的 modifier 是参数名;
  • : Modifier 冒号右边的 Modifier 是参数类型,代表参数类型是 Modifier 接口的实现类;
  • = Modifier 等号右边的 Modifier 是参数默认值,这里的 Modifier 是一个实现了 Modifier 接口,但没有没有实际作用的 Modifier 单例对象
kotlin 复制代码
interface Modifier {
    ...
    companion object : Modifier {
        ...
    }
}

不得不感叹 Kotlin 的语法糖 🍬,我们使用 companion object 一般是为了定义某些静态字段或方法。Compose 居然在 Modifier 接口里面使用 companion object 且实现了 Modifier 接口,这样一来,Modifier 这个名字就既可以用来表示 Modifier 接口,也可以用来表示那个无实际作用的 Modifier 单例对象(完整写法是 Modifier.Companion)。

then() & CombinedModifier

当你写出这样一行代码:Modifier.size(100.dp),背后发生了什么呢?点进源码跟一下:

kotlin 复制代码
fun Modifier.size(size: Dp) = this.then(
    SizeElement(...)
)

能看到 size() 是 Modifier 接口的一个扩展函数,它直接调用了 Modifier 接口的 then() 函数,传入了一个 SizeElement 对象(SizeElement 也是 Modifier 接口的实现类)。

kotlin 复制代码
interface Modifier {
    ...
    infix fun then(other: Modifier): Modifier =
        if (other === Modifier) this else CombinedModifier(this, other)
        // 如果传入的参数是无实际作用的 Modifier 单例对象,那么直接返回自己,否则利用 CombinedModifier 合并两个 Modifier: 自身 & 参数
}

class CombinedModifier(...) : Modifier { ... }

then() 函数的作用是将两个 Modifier 合并成一个 Modifier。

具体到上面的例子,合并的双方,一个是无实际作用的 Modifier 单例对象,另一个是 SizeElement 对象。二者合并后是一个 CombinedModifier 对象,这个 CombinedModifier 类也是 Modifier 接口的实现类。

kotlin 复制代码
class CombinedModifier(
    internal val outer: Modifier,
    internal val inner: Modifier
) : Modifier { ... }

此时如果在 Modifier.size(100.dp) 的后面再继续调用 .padding(10.dp),那么就会再次调用 then() 函数,传入一个 PaddingElement 对象。由 Modifier.size(100.dp) 得到的 CombinedModifier 对象,和新创建的 PaddingElement 对象,会被合并成一个新的 CombinedModifier 对象。

原来 Modifier 的链式调用,就是不断调用 Modifier.then() 函数,利用 CombinedModifier 对 Modifier 进行合并的过程啊。

Element

上面已经提到,调用 Modifier.size()Modifier.padding() 过程中会分别创建出一个 SizeElement 对象和一个 PaddingElement 对象。

其实 SizeElement 和 PaddingElement 都是通过实现 Element 接口来间接实现 Modifier 接口的。

kotlin 复制代码
interface Modifier {
    ...
    companion object : Modifier {
        ...
    }

    interface Element : Modifier {
        ...
    }
}

class CombinedModifier(
    internal val outer: Modifier,
    internal val inner: Modifier
) : Modifier { ... }

在 Compose 里面,Modifier 接口的子类,除了无实际作用的 Modifier 单例对象,和用于合并 Modifier 的 CombinedModifier 类,其他的,拥有实际作用的 Modifier 类,几乎都是通过实现 Element 接口来间接实现 Modifier 接口的。

foldIn() / foldOut() & all() / any()

kotlin 复制代码
interface Modifier {
    fun <R> foldIn(initial: R, operation: (R, Element) -> R): R
    fun <R> foldOut(initial: R, operation: (Element, R) -> R): R
    fun any(predicate: (Element) -> Boolean): Boolean
    fun all(predicate: (Element) -> Boolean): Boolean
    ...
}

Modifier 接口里面有这四个函数,它们的作用是什么呢?

all() & any()

提起 all()any(),大家应该都知道 Kotlin 集合里面,它们是用来判断集合里面的元素是否全部满足某个条件,或者是否有任意一个元素满足某个条件的。

kotlin 复制代码
val list = listOf(1, 1, 2)
val allOne = list.all { it == 1 }
val anyTwo = list.any { it == 2 }

而在 Modifier 里面,all()any() 的作用也是类似的。前面已经了解到,Modifier 链式调用后,会得到一个类似于二叉树的链表结构,每一个节点都是一个 Modifier,它的实际类型既可能是一个具有实际作用的 Modifier(Element),也可能是一个无实际作用的 Modifier 单例对象,还可能是一个专门用于合并 Modifier 的 CombinedModifier。

kotlin 复制代码
interface Modifier {
    ...
    fun all(predicate: (Element) -> Boolean): Boolean
    fun any(predicate: (Element) -> Boolean): Boolean
}

all() / any() 的作用,就是判断某个 Modifier 节点下,所包含的所有 Element 是否都满足某个条件,或者是否有任意一个 Element 满足某个条件的。可以看到 all()any() 的参数就是判断的条件,都是函数类型,接收一个 Element,判断条件后返回 Boolean。

kotlin 复制代码
class CombinedModifier(
    internal val outer: Modifier,
    internal val inner: Modifier
) : Modifier { 
    ...
    override fun any(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.any(predicate) || inner.any(predicate)

    override fun all(predicate: (Modifier.Element) -> Boolean): Boolean =
        outer.all(predicate) && inner.all(predicate)
}

因为 CombinedModifier 的内部直接包含两个 Modifier,所以 CombinedModifier 的 any()all() 函数的实现,就是拿着参数(判断条件),转发调用两个直接子节点的 any()all() 函数,并将得到的结果合并返回。 这样的实现很合理吧,如果要判断某个节点下面的所有 Element 是否都满足某个条件,是不是得对该节点所分叉出来的两个子节点进行同样的判断,然后将两个结果进行合并。

kotlin 复制代码
interface Element : Modifier {
    ...
    override fun any(predicate: (Element) -> Boolean): Boolean = predicate(this)
    override fun all(predicate: (Element) -> Boolean): Boolean = predicate(this)
}

而 CombinedModifier 直接包含的两个 Modifier,它们的实际类型可能是 Element 或 CombinedModifier。 如果是 Element,那么就直接对自己应用判断条件;如果是 CombinedModifier,那么继续递归转发调用 any()all() 函数。

kotlin 复制代码
companion object : Modifier {
    override fun any(predicate: (Element) -> Boolean): Boolean = false
    override fun all(predicate: (Element) -> Boolean): Boolean = true
}

当然,递归到最深层的那个 CombinedModifier,它所包含的两个 Modifier,其中一个是无实际作用的 Modifier 单例对象,它是妥妥的透明人,对这个递归判断过程没有任何影响,是可以忽略的。所以它的 any() 直接返回 falseall() 直接返回 true

foldIn() & foldOut()

除了 all()any(),Modifier 里面还有另外两个函数 foldIn()foldOut(), 要理解它们,首先要理解 Kotlin 里面集合的 fold() 函数。

kotlin 复制代码
data class Person(val name: String, val height: Int)

fun main() {
    val babyMike = Person(name = "Mike", height = 53)
    
    // 从出生到 18 岁,每年身高增长的厘米
    val heightGrowthEveryYear = listOf(25, 12, 8, 7, 7, 7, 6, 7, 5, 6, 7, 8, 8, 6, 3, 1, 1, 1)
    
    val adultMike = heightGrowthEveryYear.fold(babyMike) { mike, growth ->
        mike.copy(height = mike.height + growth)
    }
    
    println("Mike 出生时的身高:${babyMike.height} cm")
    println("Mike 18 岁的身高:${adultMike.height} cm")
}

Kotlin 集合里面的 fold() 函数的作用是将集合里面的元素,折叠转换成一个新的值。它接收两个参数,第一个参数是初始值,第二个参数是一个函数,这个函数接收两个参数,第一个参数是上一次折叠的结果,第二个参数是集合里面的元素,返回值是折叠后的结果。

具体到上面的例子:

  • 第一次计算:将初始值 babyMike(身高 53 cm)和第一个元素 25 (第一年增长的身高)传入函数,计算得到一岁的 Mike,身高 53 + 25 = 78 cm;

  • 第二次计算:将第一次计算的结果(一岁的 Mike,身高 78 cm)和第二个元素 12 (第二年增长的身高)传入函数,计算得到两岁的 Mike,身高 78 + 12 = 90 cm;

  • 依次类推,最后得到的结果是 adultMike,18 岁的 Mike,身高 178 cm。

如此一来,fold() 函数就将集合里面的所有元素(每年增长的身高,类型是 Int)折叠成一个新的值(18 岁的 Mike,类型和传入的初始值 babyMike 一样,都是 Person)。

就好像滚雪球一样,给定一个小雪球(初始值),然后不断滚动,每次滚动都会增加雪球的体积(集合里面的元素),最后得到一个大雪球(折叠后的结果)。

将视线转回 Modifier 接口里的 foldIn() / foldOut(),作用就是对 Modifier 链式调用后得到的 Modifier 二叉树里面的所有 Element 进行折叠转换。

可是为什么还要分成 foldIn() 和 foldOut() 两个函数呢?不是有一个 fold() 函数就够了吗?别忘了 Modifier 是顺序敏感的!正向遍历折叠,和反向遍历折叠,得到的最终结果可能并不一样。

前面我们已经知道,Modifier.size(100.dp) 相当于 Modifier then SizeElement(...),then 会将两个 Modifier 对象进行合并,得到的一个 CombinedModifier 对象。其实所谓的合并很简单,CombinedModifier 内部会使用变量 outerinner 分别保存需要合并的两个 Modifier 对象的引用地址。

kotlin 复制代码
interface Modifier {
    ...
    infix fun then(other: Modifier): Modifier =
        if (other === Modifier) this else CombinedModifier(this, other)
}

class CombinedModifier(
    internal val outer: Modifier,
    internal val inner: Modifier
) : Modifier { ... }

foldOut()

代码 Modifier.size(100.dp).padding(10.dp) 相当于 Modifier then SizeElement then PaddingElement,对应的 Modifier 二叉树如下:

如果使用 foldOut() 对根节点所包含的所有 Element 进行折叠转换,那么就是从下往上,优先折叠 inner 节点:inner.foldOut(initial, operation),将其折叠后的结果作为 initial 参数,再折叠 outer 节点:outer.foldOut(...)

在我们的例子里面,根节点的 inner 节点是一个 Element,Element 的 foldOut() 函数的实现,是直接调用 operation() 函数,将自己作为参数传入,然后返回 operation() 函数的返回值,也就是直接对自己进行折叠转换,因为它自身就是一个 Element 嘛。

kotlin 复制代码
interface Element : Modifier {
    override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R =
    	operation(this, initial)
}

根节点的 inner 节点折叠的结果,将作为折叠 outer 的初始值,如此循环往复,形成闭环,最终得到折叠转换的结果。

无实际作用的 Modifier 单例对象,它的 foldOut() 实现会直接返回 initial 参数,因为它是无实际作用的,折叠与它无关。

kotlin 复制代码
companion object : Modifier {
    override fun <R> foldOut(initial: R, operation: (Element, R) -> R): R = initial
}

foldIn()

foldIn()foldOut() 的区别就是,foldIn() 是从节点开始,优先折叠 outer 节点,再折叠 inner 节点,过程就不再赘述了。

END.

了解 Modifier 里面的 then()CombinedModifier,能够帮助我们更好地理解 Modifier 链式调用的工作原理。all()any()foldOut()foldInt() 这些函数,则让我们窥探到 Modifier 链背后的执行逻辑。

相关推荐
诸神黄昏EX1 小时前
Android 分区相关介绍
android
大白要努力!2 小时前
android 使用SQLiteOpenHelper 如何优化数据库的性能
android·数据库·oracle
Estar.Lee2 小时前
时间操作[取当前北京时间]免费API接口教程
android·网络·后端·网络协议·tcp/ip
Winston Wood2 小时前
Perfetto学习大全
android·性能优化·perfetto
Dnelic-5 小时前
【单元测试】【Android】JUnit 4 和 JUnit 5 的差异记录
android·junit·单元测试·android studio·自学笔记
Eastsea.Chen7 小时前
MTK Android12 user版本MtkLogger
android·framework
长亭外的少年15 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
JIAY_WX15 小时前
kotlin
开发语言·kotlin
建群新人小猿17 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神18 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri