Compose原理十二之CompositionLocal

一、前言

在 Jetpack Compose 的开发中,我们经常遇到这样的场景:有一个数据(比如主题颜色、字体、上下文 Context 等),需要在多层嵌套的组件中进行传递。如果通过普通的函数参数一层层传递(所谓的"Prop Drilling"),会导致中间许多并不需要这个数据的组件也被迫增加参数,代码极其冗余和难以维护。

这时,CompositionLocal 就闪亮登场了。

二、CompositionLocal 是干嘛的?为什么要用它?

1. 它是干嘛的? CompositionLocal 是 Compose 提供的一种隐式传参机制。它允许你在树的某个高层节点"提供(Provide)"一个值,然后在树的底层任何一个节点直接"消费(Consume)"这个值,而不需要通过函数参数显式地一层层传递。

2. 为什么要用它?

  • 避免属性透传 (Prop Drilling): 比如 LocalContext,几乎任何 UI 组件都可能需要 Context,如果作为参数传递,那么所有的 @Composable 函数都要带上 Context 参数,简直是噩梦。
  • 作用域隔离: 它的值是与组件树关联的,即子树中可以覆盖父树中提供的值,不同的子树可以读取到不同的值。
  • 状态响应: 结合 Compose 的重组机制,当 CompositionLocal 提供的状态发生变化时,只会触发读取了该值的组件进行精确重组。

三、从源码看 CompositionLocal 的工作原理

要理解 CompositionLocal 的原理,我们需要弄清楚三个核心问题:

  1. 它是如何被创建的?
  2. 它是如何被提供的 (Provide)?
  3. 它是如何被读取的 (Consume)?

3、1 它是如何被创建的?

在 Compose 中,我们通常使用 compositionLocalOf 或 staticCompositionLocalOf 来创建一个 ProvidableCompositionLocal。

compose-sources/commonMain/androidx/compose/runtime/CompositionLocal.kt 中定义了创建函数:

kotlin 复制代码
public fun <T> compositionLocalOf(
    policy: SnapshotMutationPolicy<T> = structuralEqualityPolicy(),
    defaultFactory: () -> T
): ProvidableCompositionLocal<T> = DynamicProvidableCompositionLocal(policy, defaultFactory)

public fun <T> staticCompositionLocalOf(
    defaultFactory: () -> T
): ProvidableCompositionLocal<T> = StaticProvidableCompositionLocal(defaultFactory)

这两者的区别在于它们对应的内部实现类:

  • DynamicProvidableCompositionLocal (动态): 内部其实包裹了一个 MutableState。当其值发生变化时,只有真正读取 了该值的 @Composable 才会发生重组(精准刷新)。
  • StaticProvidableCompositionLocal (静态): 内部直接存储值,没有任何 State 追踪。当提供给它的值发生变化时,整个 CompositionLocalProvider 包裹的所有内容都会发生重组。适用于极少改变的数据。

我们可以看一下 ProvidableCompositionLocal 的源码定义:

kotlin 复制代码
public abstract class ProvidableCompositionLocal<T> internal constructor(
    defaultFactory: () -> T
) : CompositionLocal<T>(defaultFactory) {
    // 允许通过 `provides` 语法糖生成一个 ProvidedValue
    @Suppress("UNCHECKED_CAST")
    public infix fun provides(value: T): ProvidedValue<T> =
        ProvidedValue(this, value, true)
}

3、2 它是如何被提供的?(Provider 的秘密)

我们平时这样使用:

kotlin 复制代码
CompositionLocalProvider(LocalThemeColor provides Color.Red) {
    MyComponent()
}

来看看 CompositionLocalProvider 的源码 (CompositionLocal.kt):

kotlin 复制代码
@Composable
public fun CompositionLocalProvider(
    vararg values: ProvidedValue<*>,
    content: @Composable () -> Unit
) {
    currentComposer.startProviders(values)
    content()
    currentComposer.endProviders()
}

极其简单!调用了 currentComposer.startProviders(values)。Composer 是 Compose 运行时的核心。当调用 startProviders 时,它会将你传入的这些 ProvidedValue 合并到一个字典中:CompositionLocalMap。

在 compose-sources/commonMain/androidx/compose/runtime/CompositionLocalMap.kt 中,CompositionLocalMap 被定义为一个不可变的 Map 快照:

kotlin 复制代码
public sealed interface CompositionLocalMap {
    public operator fun <T> get(key: CompositionLocal<T>): T
}

在 Composer 的具体实现中,每当遇到 startProviders,它就会基于当前父级的 CompositionLocalMap,结合新传入的 values,生成一个新的 CompositionLocalMap,并将其压入栈中。这样,在 content() 内部的所有组件,看到的都是这个包含了新值的 Map。这就实现了作用域与覆盖的特性。

3、3 它是如何被读取的?

当我们在组件中读取值时:

kotlin 复制代码
val color = LocalThemeColor.current

这里的 .current 是一个定义在 CompositionLocal 上的只读属性。我们来看看 CompositionLocal.kt:

kotlin 复制代码
public sealed class CompositionLocal<T> constructor(
    defaultFactory: () -> T
) {
    @get:Composable
    @ReadOnlyComposable
    public inline val current: T
        get() = currentComposer.consume(this)
}

秘密就在这里! 首先,它被 @get:Composable 标记,这意味着只能在 Composable 函数或另一个 Composable getter 中读取它 。 其次,它直接调用了 currentComposer.consume(this)。

我们不需要再深入 ComposerImpl 几千行的源码,简单来说,consume(this) 做了两件事:

  1. 查表: 从 Composer 当前作用域绑定的 CompositionLocalMap 中,以当前 CompositionLocal 实例本身为 Key 去查找对应的值(ValueHolder)。如果没有找到,就执行我们创建它时传入的 defaultFactory 获取默认值。
  2. 建立追踪依赖 (对于 DynamicLocal): 如果这个 Local 是 compositionLocalOf 创建的动态类型,它取出的其实是一个 State。读取 State 的值时,Compose 的快照系统(Snapshot System)就会自动记录下:"当前的 Composable 读取了这个 State"。

这样一来,当更高层的 Provider 改变了提供的值,如果是动态 Local,只有订阅了这个 State 的 Composable 会被重组;如果是静态 Local,由于没有 State 记录,系统只能粗暴地把整个 Provider 的 content 全部重组。

四、总结

  1. CompositionLocal 解决了参数层层传递的问题,本质是通过 Composer 维护的一个按作用域层级叠加上下文的 Map (CompositionLocalMap)。
  2. 提供值 (Provide): 将新的键值对合并到当前层级的 Map 中,作用于子树。
  3. 获取值 (Consume): 通过隐式的 currentComposer 去查询当前层级的 Map。
  4. 响应更新:
    • compositionLocalOf 基于内部维护的 State 实现了精准重组。
    • staticCompositionLocalOf 舍弃了 State 跟踪开销,但更新时会触发全局重组,适合那些"一旦提供几乎不改变"的数据(如 Context)。
相关推荐
数字营销观察4 分钟前
geo管理软件选型指南:从技术架构到场景落地的核心考量
架构
青峰讲运维23 分钟前
OpenStack Nova 计算服务深度解析|原理 + 架构 + 完整实操
架构·openstack
深入云栈42 分钟前
Netty 4.2.x 源码深度解析 (二十一):Pipeline 事件传播 —— Inbound 与 Outbound 的双向流转
java·后端·架构
FYKJ_20101 小时前
springboot高考志愿填报推荐系统14332-计算机课程设计、毕业设计
javascript·spring boot·python·架构·django·课程设计·高考
找了一圈尾巴1 小时前
Agent 架构的记忆系统:从滑动窗口到离线整合,一套分层记忆设计
架构·agent
诚心呈意共享外卖配送系统3 小时前
怎么搭建同城聚合配送平台?一套接单、派单、算账的分层系统架构设计
架构·系统架构·创业创新
国科安芯3 小时前
从点火到入轨的毫秒尺度:商业运载火箭飞行时序控制中的抗辐射MCU可靠性边界—— 面向短时高可靠任务的单粒子效应证据与架构分析
网络·人工智能·单片机·嵌入式硬件·架构·火箭·抗辐射
Dola_Zou3 小时前
从二进制加固到离线鉴权 工业边缘AI安全防御架构
人工智能·安全·ai·架构·软件加密
严同学正在努力3 小时前
PostgreSQL 高可用教程(流复制 + Patroni 生产级实战)
运维·数据库·ai·postgresql·架构
Dawson Zhu3 小时前
【AI编译器入门②】TVM架构拆解:Relay、Relax、TIR与Runtime四层结构
人工智能·语言模型·架构·aigc·agi