基本概念
ModifierLocal 中的 “Local” 和 CompositionLocal、ThreadLocal 中的 “Local” 的含义一样,都表示局部变量。
ThreadLocal 的作用是提供线程局部变量,它会给每一个线程分配一个独立的 ThreadLocal 对象,在每个线程中,只能访问独属于自己的 ThreadLocal 对象,线程间互不干扰。
CompositionLocal 的作用是提供可穿透 Composable 函数层级的局部变量,它会在某个 Composition 节点提供值,然后该节点下的所有子组件都可以访问这个值,无需通过每个子组件的函数参数来传递这个值。
而 ModifierLocal 也是类似的作用,只不过,它穿透的是 Modifier 链,它可以在 Modifier 链中从外到内地传递数据。
为什么需要 ModifierLocal?
因为多个 Modifier 之间是无法直接共享数据的,每个 Modifier 都是独立的函数作用域。比如在下面的示例代码中,内层的 layout
中无法访问到外层的 layout
中创建的 size
对象。
@Composable
private fun ModifierSharedDataDemo() {
Modifier
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
val size = "${placeable.width} x ${placeable.height}" // 📌 创建 size 对象
// 由于size是局部变量,只在当前函数作用域内可见
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
size // ❌ 获取不到 size 对象,因为它们是不同的函数作用域
layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
}
外层layout
中创建的size
对象是一个局部变量,仅在该函数体内可见,内层的layout
无法访问。
ModifierLocal 就是用来突破这个限制,实现 Modifier 链中数据共享的,我们来看看基本写法。
ModifierLocal 基本写法
只需使用 modifierLocalProvider()
修饰符来提供数据,modifierLocalConsumer()
修饰符来消费数据就可以了。
不过提供数据时,要使用 modifierLocalOf()
函数来创建 ModifierLocal 实例,设置默认值。
val sizeKey = modifierLocalOf { "0 x 0" } // 创建 ModifierLocal 实例 `sizeKey`,默认值为 "0 x 0"
Modifier
.modifierLocalProvider(key = sizeKey, value = { "400 x 300" }) // 提供一个新值 "400 x 300"
.modifierLocalConsumer(consumer = {
println("size is ${sizeKey.current}") // 读取并使用这个值
})
这样我们就实现了数据在多个 Modifier 之间的共享。
ModifierLocal 进阶实现
然后我们来看这种写法,怎么用到最开始的示例中。
首先,也要创建一个 ModifierLocal 实例作为数据的键。但不能像上面一样,直接使用modifierLocalProvider()
、modifierLocalConsumer()
修饰符,虽然这样可以让它们之间进行共享数据,但是无法在 layout()
修饰符之间共享数据,所以这是没有意义的。
所以我们要实现modifierLocalProvider()
、modifierLocalConsumer()
修饰符背后的ModifierLocalProvider
、ModifierLocalConsumer
接口,同时实现 layout()
修饰符背后的接口 LayoutModifierNode
,像这样:
@Composable
private fun ModifierLocalSharedDataDemo() {
val sizeKey = modifierLocalOf { "0 x 0" }
Modifier
.then(object : LayoutModifierNode, ModifierLocalProvider<String>, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
TODO("Not yet implemented")
}
override val key: ProvidableModifierLocal<String>
get() = TODO("Not yet implemented") // 提供key
override val value: String
get() = TODO("Not yet implemented") // 提供value
})
.then(object : LayoutModifierNode, ModifierLocalConsumer, Modifier.Node() {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
TODO("Not yet implemented")
}
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
// 读取共享数据
TODO("Not yet implemented")
}
})
}
然后依次实现每个方法就可以了。
@Composable
private fun ModifierLocalSharedDataDemo() {
val sizeKey = modifierLocalOf { "0 x 0" }
Modifier
.then(object : LayoutModifierNode, ModifierLocalProvider<String>, Modifier.Node() {
lateinit var size: String
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
// 初始化数据
size =
"${placeable.width} x ${placeable.height}" // 把 size 变量变为成员变量,这样在 value 的 get() 函数中才可以获取到
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
override val key: ProvidableModifierLocal<String> // 数据的键值
get() = sizeKey
override val value: String // 提供的数据
get() = size
})
.then(object : LayoutModifierNode, ModifierLocalConsumer, Modifier.Node() {
lateinit var size: String
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
println(size) // 使用共享数据
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
// 消费数据
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) { // 在 ModifierLocalReadScope 的上下文中获取传递的数据
size = sizeKey.current // size 作为成员变量,方便在 measure() 函数中获取
}
}
})
}
这样就搞定了?
其实没有, size
数据会在测量阶段被初始化,在它的 get() 函数中被提供给下游,而onModifierLocalsUpdated
回调在测量阶段之前就已经执行完毕了,这会导致下游在 onModifierLocalsUpdated
中获取到的是未初始化的数据,抛出一个变量未经初始化的异常。
简要来说就是数据的使用会先于数据的初始化。
在这种情况下,我们的解决方案是,把数据包在引用类型中,比如说数组,然后再传递到下游。这样即使数据还没有初始化,下游也能拿到数据的引用,等上游初始化后,下游自然能看到最新值。
比如:
@Composable
private fun ModifierLocalSharedDataDemo() {
// 使用数组包装数据
val sizeKey = modifierLocalOf { arrayOf("0 x 0") }
Modifier
.then(object : LayoutModifierNode, ModifierLocalProvider<Array<String>>, Modifier.Node() {
// 预先初始化数组引用
var size: Array<String> = arrayOf("")
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
size[0] =
"${placeable.width} x ${placeable.height}" // 在测量阶段更新数据内容
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
override val key: ProvidableModifierLocal<Array<String>>
get() = sizeKey
override val value: Array<String>
get() = size // 返回数组引用
})
.then(object : LayoutModifierNode, ModifierLocalConsumer, Modifier.Node() {
lateinit var size: Array<String> // 修改 Array<String>
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
println(size[0]) // 此时数组内容已被上游更新
return layout(placeable.width, placeable.height) {
placeable.placeRelative(0, 0)
}
}
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) {
// 获取数组引用
size = sizeKey.current
// 此时数组中的数据可能还未被上游更新
}
}
})
}
这种方式利用了引用类型的特性,下游获取的是数组的引用而非内容的副本,因此即使在获取引用后,上游修改了数组内容,下游仍能看到最新值。
多级连续消费模式
很多时候,我们还会同时实现 ModifierLocalProvider
、ModifierLocalConsumer
接口,来实现多级的连续数据传递和处理。这时,一个 Modifier 可以同时作为数据的消费者和提供者,形成一个数据处理链。
比如 Compose 中的 windowInsetsPadding()
修饰符,它的作用是给组件添加内边距,并且它可以连续调用。每一级都会获取上游提供的内边距数据,与自身的内边距累加,然后传递给下游。
Modifier
.windowInsetsPadding(
WindowInsets(
left = 5.dp,
top = 10.dp,
right = 5.dp,
bottom = 10.dp
)
)
.windowInsetsPadding(
WindowInsets(
left = 2.dp,
top = 2.dp,
right = 2.dp,
bottom = 2.dp
)
)
.windowInsetsPadding(
WindowInsets(
left = 1.dp,
top = 1.dp,
right = 1.dp,
bottom = 2.dp
)
)
这种连续调用的效果是叠加的:
左边距: 5.dp + 2.dp + 1.dp = 8.dp
上边距: 10.dp + 2.dp + 1.dp = 13.dp
右边距: 5.dp + 2.dp + 1.dp = 8.dp
下边距: 10.dp + 2.dp + 2.dp = 14.dp
注意事项:在实现多级连续消费时,必须在获取上游数据后立即完成数据处理,确保下游获取数据时,数据已经初始化。