ModifierLocal:Compose 中的 Modifier 数据传递机制

133 阅读3分钟

基本概念

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() 修饰符背后的ModifierLocalProviderModifierLocalConsumer 接口,同时实现 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 
                    // 此时数组中的数据可能还未被上游更新
                }
            }

        })

}

这种方式利用了引用类型的特性,下游获取的是数组的引用而非内容的副本,因此即使在获取引用后,上游修改了数组内容,下游仍能看到最新值。

多级连续消费模式

很多时候,我们还会同时实现 ModifierLocalProviderModifierLocalConsumer 接口,来实现多级的连续数据传递和处理。这时,一个 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

注意事项:在实现多级连续消费时,必须在获取上游数据后立即完成数据处理,确保下游获取数据时,数据已经初始化。