反思
我们上面说的都是使用方面的,那么你有没有想过?为什么官方自定义设计系统要使用 CompositionLocal
呢?
可能有新同学并没有使用过这个,为了便于更好理解,首先我们先搞清楚 CompositionLocal
是干什么的,先不说其通俗概念,我们简单用个小例子就能讲明白。
解构
在常见的开发场景中,我们很多时候,经常会将某个参数传递给其他方法,我们称之为显示传递。
切换一下场景,我们在 Compose
中,经常会给可组合函数传递参数,于是这个方式被 Google 学术化称为: 数据以参数的形式 向下流经 整个界面树传递给每个可组合函数 ,就如下述所示:
@Composable fun A(message: String) { Column() { B(message) } } @Composable fun B(message: String) { Text(text = message) C(message) } @Composable fun C(message: String) { Text(text = message) }
上述示例中有3个可组合函数,其中 A 需要接收一个 message
字符串,并且将其传递给 B ,而 B 同时又需要传递给 C ,类似于无限套娃一样,此时我们可能感觉还行吧,但是如果这种套娃出现 n 层呢,但是数据如果不止一个呢?此时将可能非常麻烦。
那么有没有其他方式解决呢?在 Compose
中,官方给出了标准答案,那就是 CompositionLocal
:
即使用 CompositionLocal
来完成 composable
树中的数据共享,并且 CompositionLocal
具有层级,它可以被限定在某个 composable
作为根节点的子树中,默认向下传递,同时子树中的某个 composable
也可以对该 CompositionLocal
进行覆盖,然后这个新值就会在这个 composable
中继续向下传递。
composable
即可组合函数,简单理解就是使用了 @Composable
标注的方法。
实践
如下所示,我们对上述代码使用 CompositionLocal
进行改造:
val MessageContent = compositionLocalOf { "simple" } @Composable fun A(message: String) { Column() { // provides 相当于写入数据 CompositionLocalProvider(MessageContent provides message) { B() } } } @Composable fun B() { Text(text = MessageContent.current) CompositionLocalProvider(MessageContent provides "临时对值进行更改") { C() } } @Composable fun C() { Text(text = MessageContent.current) }
先定义了一个名为 MessageContent
的 CompositionLocal
,其默认值为 “simple” , A 方法接收一个 message 字段,并且将其写入 MessageContent
,然后在 B 中,我们就可以获取到刚才方法 A 中写入到CompositionLocal的数据,而无需显示的在方法参数里增加字段。
同样,方法B也可以对这个 CompositionLocal
进行更改,这样 C 就会拿到另一个值。
并且当我们使用 CompositionLocal.current
来获取数据的时候,这个 current
会返回距离当前组件最近的那一个值,所以我们也可以利用其来做一些隐式分离的基础实现。
扩展
相应的,我们再说一下创建 CompositionLocal
的方式:
compositionLocalOf
:在重组期间更改提供的值只会使读取其current
值的内容无效。staticCompositionLocalOf
:与compositionLocalOf
不同,Compose
不会跟踪staticCompositionLocalOf
的读取。更改该值会导致提供CompositionLocal
的整个contentlambda
被重组,而不仅仅是在组合中读取current
值的位置。
总结
我们在上面大概了解了 CompositionLocal
的作用,试想一下,如果不用它,如果让我们自己去实现一个颜色系统,可能就会陷入我们最开始那种 随心所欲 的写法。
首先,那种写法可以用吗?当然可以用,但是实际中问题会很多,比如说主题的更改会导致而且不符合 Compose
的设计,而且如果我们可能有一部分业务在一定情况下,它可能一直保持一个主题色,那么此时怎么解决?
如果是方法1,可能此时会进入硬编码阶段,即使用复杂的业务逻辑去完成; 但如果是利用 CompositionLocal
呢?这个问题还会存在吗,只需要写入一个新的颜色配置即可,这个逻辑结束后再重新写入当前主题配置即可,还会存在复杂的逻辑缠绕情况吗?
这也就是为什么 Google
选择使用 CompositionLocal
去自定义颜色系统以及整个主题系统中可以供用户操纵的配置,即隐式,对使用者而言,无感知的就可以办到。
深入分析
看完了 CompositionLocal
的妙用与实际场景,我们不妨想一想,CompositionLocal
到底是怎么实现的,所谓知其然知其所以然。不深入源码往往很难理解具体实现,所以这部分的解析可能略显复杂。
CompositionLocal
言归正传,我们先看一眼源码,相应的注释与代码如下:
sealed class CompositionLocal<T> constructor(defaultFactory: () -> T) { // 默认值 internal val defaultValueHolder = LazyValueHolder(defaultFactory) // 写入最新数据 @Composable internal abstract fun provided(value: T): State<T> // 返回由最近的 CompositionLocalProvider 提供的值 @OptIn(InternalComposeApi::class) inline val current: T @ReadOnlyComposable @Composable //直接追这里代码 get() = currentComposer.consume(this) }
我们知道获取数据的时候使用的 current
,那么直接追这里即可。
currentComposer.consume()
@InternalComposeApi override fun <T> consume(key: CompositionLocal<T>): T = // currentCompositionLocalScope() 获取父可组合项提供的当前CompositionLocal范围map resolveCompositionLocal(key, currentCompositionLocalScope())
这段代码主要用于解析本地可组合项,从而获得数据。我们先看里面的 currentCompositionLocalScope()
currentCompositionLocalScope()
private fun currentCompositionLocalScope(): CompositionLocalMap { //如果可组合项当前正在插入并且有数据提供者 if (inserting && hasProvider) { // 从插入表中取离当前 composable 最近的group(可以理解为直接去取index最近的currentGroup) var current = writer.parent // 如果这个group不是空的 while (current > 0) { // 在插槽表中取出这个group的key与当前composable的key进行对比 if (writer.groupKey(current) == compositionLocalMapKey && writer.groupObjectKey(current) == compositionLocalMap ) { // 返回指定位置的 CompositionLocalMap return writer.groupAux(current) as CompositionLocalMap } // 死循环,不断的向上找,如果当前组里没有,就继续向上找,直到找到可以与当前匹配的 current = writer.parent(current) } } //如果当前composable的slotTable内的数组不为空 if (slotTable.groupsSize > 0) { //从当前插槽中取里当前离 composable 最近的graoup var current = reader.parent // 默认值为-1,如果存在,即意味着存在可组合项 while (current > 0) { if (reader.groupKey(current) == compositionLocalMapKey && reader.groupObjectKey(current) == compositionLocalMap ) { //从providerUpdates数组中获取当前CompositionLocalMap,插入见 - startProviders return providerUpdates[current] ?: reader.groupAux(current) as CompositionLocalMap } current = reader.parent(current) } } //如果没找到,则返回父 composable 的 Provider return parentProvider }
用于获取离当前 composable
最近的 CompositionLocalMap
。
resolveCompositionLocal()
private fun <T> resolveCompositionLocal( key: CompositionLocal<T>, scope: CompositionLocalMap ): T = if (scope.contains(key)) { // 如果当前父CompositionLocalMap里包含了当前local,直接从map中取 scope.getValueOf(key) } else { // 否则也就意味着其目前就是最顶层,没有父local,直接使用默认值 key.defaultValueHolder.value }
使用当前 CompositionLocal
当做 key ,然后去离当前最近的 CompositionLocalMap
里查找对应的 value ,如果有直接返回,否则使用 CompositionLocal
的默认值。
总结
所以当我们使用 CompositionLocal.current
获取数据时,内部其实是通过 currentCompositionLocalScope()
获取父CompositionLocalMap
,注意,这里为什么是map呢? 因为这里获取的是当前父可组合函数下 所有 的 CompositionLocal
,所以源码里的consume
方法参数里需要传递当前 CompositionLocal
进去,判断当前我们要取的 local
有没有存在在其中,如果有,则直接取,否则使用默认值。
那问题来了,我们的 CompositionLocal是怎么被可组合树保存的呢? 带着这个问题,我们继续往下深究。
CompositionLocalProvider
要想知道 CompositionLocal
是如何被可组合树保存,必须从下面开始看起。
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) { currentComposer.startProviders(values) content() currentComposer.endProviders() }
这里的 currentComposer
又是什么?而且为什么先start,后end呢?
我们点进去 currentComposer
看看。
/** * Composer is the interface that is targeted by the Compose Kotlin compiler plugin and used by * code generation helpers. It is highly recommended that direct calls these be avoided as the * runtime assumes that the calls are generated by the compiler and contain only a minimum amount * of state validation. */ interface Composer 👇 internal class ComposerImpl : Composer
可以看到在 Composer
的定义里, Composer
是为 Compose kotlin 编译器插件 提供的,google强烈不建议我们自己手动调用,也就是说,这里的 start
和 end
其实就是两个标记,编译器会自己调用,或者说就是为了方便编译器。接着我们去看 startProviders()
startProviders
@InternalComposeApi override fun startProviders(values: Array<out ProvidedValue<*>>) { //获得当前Composable下的CompositionLocal-Map val parentScope = currentCompositionLocalScope() ... val currentProviders = invokeComposableForResult(this) { compositionLocalMapOf(values, parentScope) } val providers: CompositionLocalMap val invalid: Boolean //如果可组合项正在插入树中或者其是第一次被调用,则为true if (inserting) { //更新当前的CompositionLocalMap providers = updateProviderMapGroup(parentScope, currentProviders) invalid = false hasProvider = true } else { //如果当前可组合项不可以跳过(即发生了改变)或者providers不相同,则更新当前Composable的group if (!skipping || oldValues != currentProviders) { providers = updateProviderMapGroup(parentScope, currentProviders) invalid = providers != oldScope } else { //否则跳过当前更新 skipGroup() providers = oldScope invalid = false } } //如果本次重组无效且没有正在插入,更新当前group的 CompositionLocalMap if (invalid && !inserting) { providerUpdates[reader.currentGroup] = providers } // 将当前的操作push到栈中,后续还要再弹出 providersInvalidStack.push(providersInvalid.asInt()) ... // 将providers数据写入group,最终会写入到SlotTable-slots(插槽的缓冲区)数组中 start(compositionLocalMapKey, compositionLocalMap, false, providers) }
这个方法用于启动数据提供者,如果看过 compose
的设计原理就会知道这里其实就相当于 group
的一个开始标记,其内部的内容主要是先获取离当前 composable
最近的 CompositionLocalMap
,compositionLocalMapOf() 将当前传递进来的 value
更新到对应的 CompositionLocalMap
中并返回,然后将这个map再更新到当前group中。
相应的我们说了,这是一个开始标记,自然也存在一个终止标记,即 end,在上述源码中,我们可以知道,其就是 endProviders():
endProviders
override fun endProviders() { endGroup() endGroup() // 将当前的操作出栈 providersInvalid = providersInvalidStack.pop().asBool() }
总结
当我们使用 CompositionLocalProvider
将数据绑定到 CompositionLocal
时,其内部会将其保存到距离当前 composable
最近的 CompositionLocalMap
里面,当我们后续想要使用的时候,CompositionLocal.current
读取数据时,其会去查找对应的 CompositionLocalMap
,并且以我们的 CompositionLocal
为 key,如果存在则返回,否则使用默认值返回。
碎碎念
本文其实并不是特别难的问题,但却是 Compose 在实际中会遇到的一个问题,解决这个问题很简单,但理解背后的设计却是更重要的,希望通过本文,大家可以更好的理解 CompositionLocal 在实际中的场景以及设计理念。当然了解具体的源码其实还是需要了解 Compose 的基础设计,这点参考文章下方贴出的Android开发者链接即可。后续我将继续深追 Compose 在实际应用中需要解决的问题并且分析思路,如果有错误,希望不吝赐教。