10、CompositionLocal

45 阅读4分钟

使用 CompositionLocal 将数据的作用域限定在局部

CompositionLocal 是 Jetpack Compose 中用于在 UI 层次结构中隐式传递数据的机制,允许在不通过显式参数传递的情况下,将数据从父级组件传递到深层嵌套的子级组件。

为什么使用 CompositionLocal

通常情况下,在 Compose 中,数据以参数形式向下流经整个界面树。但对于广泛使用的常用数据(如主题颜色、排版样式等),这种方式会很繁琐。

@Composable fun MyApp() {
    // 主题信息通常在应用根部定义
    val colors = colors()
}

// 深层嵌套的可组合项
@Composable fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        color = colors.onPrimary // ← 需要访问 colors
    )
}

CompositionLocal 允许创建以树为作用域的具名对象,作为数据流经界面树的隐式方式。

CompositionLocal 基础

CompositionLocal 通常在界面树的某个节点提供值,该值可被其后代使用,无需在每个可组合函数中显式声明为参数。

Material 主题在后台使用 CompositionLocalMaterialTheme 对象提供了三个 CompositionLocal 实例:colorSchemetypographyshapes

@Composable fun MyApp() {
    // 提供一个 Theme,其值会向 content 传播
    MaterialTheme {
        // colorScheme、typography 和 shapes 的新值在这里可用
        // ... 内容 ...
    }
}

// MaterialTheme 层次结构深处的某个可组合项
@Composable fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // 从 MaterialTheme 的 LocalColors CompositionLocal 获取 primary 颜色
        color = MaterialTheme.colorScheme.primary
    )
}

提供和使用 CompositionLocal

CompositionLocal 实例的作用域限定为组合的一部分,可以在树的不同级别提供不同的值。CompositionLocalcurrent 值对应于该组合部分中最接近的祖先提供的值。

使用 CompositionLocalProvider 及其 provides 函数为 CompositionLocal 提供新值:

@Composable fun CompositionLocalExample() {
    MaterialTheme {
        Surface {
            Column {
                Text("Uses Surface's provided content color")
                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
                    Text("Primary color provided by LocalContentColor")
                    Text("This Text also uses primary as textColor")
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
                        DescendantExample()
                    }
                }
            }
        }
    }
}

@Composable fun DescendantExample() {
    // CompositionLocalProviders 也适用于跨可组合函数
    Text("This Text uses the error color now")
}

在访问 CompositionLocal 时,使用其 current 属性:

@Composable fun FruitText(fruitSize: Int) {
    // 从 LocalContext 的当前值获取 resources
    val resources = LocalContext.current.resources
    val fruitText = remember(resources, fruitSize) {
        resources.getQuantityString(R.plurals.fruit_title, fruitSize)
    }
    Text(text = fruitText)
}

创建自定义 CompositionLocal

有两个 API 可用于创建 CompositionLocal

  1. compositionLocalOf:在重组期间更改提供的值只会使读取其 current 值的内容无效
  2. staticCompositionLocalOf:Compose 不会跟踪其读取。更改该值会导致提供 CompositionLocal 的整个 content lambda 被重组,适用于很少或永远不会更改的值

示例:创建一个用于管理高度的 CompositionLocal

// LocalElevations.kt 文件
data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// 定义一个具有默认值的全局 CompositionLocal 对象
// 该实例可被应用中的所有可组合项访问
val LocalElevations = compositionLocalOf { Elevations() }

为 CompositionLocal 提供值

使用 CompositionLocalProvider 将值绑定到给定层次结构的 CompositionLocal 实例:

// MyActivity.kt 文件
class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            // 根据系统主题计算高度
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }
            
            // 将 elevation 绑定为 LocalElevations 的值
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... 内容 ...
                // 在组合的这部分访问 LocalElevations.current 时
                // 会看到 elevations 实例
            }
        }
    }
}

使用 CompositionLocal

访问由最接近的 CompositionLocalProvider 提供的值:

@Composable fun SomeComposable() {
    // 访问全局定义的 LocalElevations 变量
    // 获取组合这部分中的当前 Elevations
    MyCard(elevation = LocalElevations.current.card) {
        // 内容
    }
}

替代方案

CompositionLocal 并非总是最佳解决方案。以下是一些替代方案:

1. 传递显式参数

显式使用可组合项的依赖项是良好实践,只传递所需内容:

@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel.data)
}

// 不要传递整个对象!只传递后代需要的内容。
// 也不要使用 CompositionLocal 隐式传递 ViewModel 依赖项。
@Composable fun MyDescendant(myViewModel: MyViewModel) { /* ... */ }

// 只传递后代需要的内容
@Composable fun MyDescendant(data: DataToDisplay) {
    // 显示数据
}

2. 控制反转

不是由后代接受依赖项执行逻辑,而是让父级负责执行逻辑:

@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(onLoadClick = { myViewModel.loadData() })
}

@Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

3. 使用内容 lambda

同样,@Composable 内容 lambda 也可以达到相同效果:

@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(content = {
        Button(onClick = { myViewModel.loadData() }) {
            Text("Confirm")
        }
    })
}

@Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}

最佳实践

  • CompositionLocal 应具有合适的默认值
  • 仅在真正需要时使用 CompositionLocal(如横切关注点)
  • 避免为特定屏幕的 ViewModel 创建 CompositionLocal
  • 遵循"状态向下流动,事件向上流动"的模式
  • 谨慎使用,过度使用会使可组合项的行为难以推断

CompositionLocal 非常适合基础架构,Jetpack Compose 本身大量使用该工具,但在应用代码中应谨慎使用。