使用 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 主题在后台使用 CompositionLocal。MaterialTheme 对象提供了三个 CompositionLocal 实例:colorScheme、typography 和 shapes。
@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 实例的作用域限定为组合的一部分,可以在树的不同级别提供不同的值。CompositionLocal 的 current 值对应于该组合部分中最接近的祖先提供的值。
使用 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:
compositionLocalOf:在重组期间更改提供的值只会使读取其current值的内容无效staticCompositionLocalOf:Compose 不会跟踪其读取。更改该值会导致提供CompositionLocal的整个contentlambda 被重组,适用于很少或永远不会更改的值
示例:创建一个用于管理高度的 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 本身大量使用该工具,但在应用代码中应谨慎使用。