CompositionLocal
是一种通过组合隐式向下传递数据的工具。在本页面中,您将更详细地了解 CompositionLocal
是什么,如何创建自己的 CompositionLocal
,以及何时 CompositionLocal
是适合您的用例的良好解决方案。
CompositionLocal
简介
通常在 Compose 中,数据通过 UI 树作为参数流向每个可组合函数。这使得可组合项的依赖项明确。然而,对于颜色或字体样式等频繁和广泛使用的数据来说,这可能很麻烦。请看以下示例:
@Composable fun MyApp() { // Theme information tends to be defined near the root of the application val colors = colors() } // Some composable deep in the hierarchy @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, color = colors.onPrimary // ← need to access colors here ) }
为了支持无需将颜色作为显式参数依赖项传递给大多数可组合项,Compose 提供了 CompositionLocal
,它允许您创建树作用域的命名对象,这些对象可以用作数据在 UI 树中隐式流动的途径。
CompositionLocal
元素通常在 UI 树的某个节点中提供一个值。该值可以被其可组合后代使用,而无需在可组合函数中将 CompositionLocal
声明为参数。
CompositionLocal
是 Material 主题在底层使用的工具。MaterialTheme
是一个提供三个 CompositionLocal
实例的对象:colorScheme
、typography
和 shapes
,允许您稍后在组合的任何后代部分检索它们。具体来说,它们是您可以通过 MaterialTheme
的 colorScheme
、shapes
和 typography
属性访问的 LocalColorScheme
、LocalShapes
和 LocalTypography
属性。
@Composable fun MyApp() { // Provides a Theme whose values are propagated down its `content` MaterialTheme { // New values for colorScheme, typography, and shapes are available // in MaterialTheme's content lambda. // ... content here ... } } // Some composable deep in the hierarchy of MaterialTheme @Composable fun SomeTextLabel(labelText: String) { Text( text = labelText, // `primary` is obtained from MaterialTheme's // LocalColors CompositionLocal color = MaterialTheme.colorScheme.primary ) }
一个 CompositionLocal
实例被限定在组合的某个部分,因此您可以在树的不同级别提供不同的值。CompositionLocal
的 current
值对应于在该组合部分中由祖先提供的最接近的值。
要为 CompositionLocal
提供新值,请使用 CompositionLocalProvider
及其 provides
中缀函数,该函数将 CompositionLocal
键与 value
关联起来。CompositionLocalProvider
的 content
lambda 在访问 CompositionLocal
的 current
属性时将获得所提供的值。当提供新值时,Compose 会重新组合读取 CompositionLocal
的组合部分。
作为示例,LocalContentColor
CompositionLocal
包含用于文本和图标的首选内容颜色,以确保其与当前背景色形成对比。在以下示例中,CompositionLocalProvider
用于为组合的不同部分提供不同的值。
@Composable fun CompositionLocalExample() { MaterialTheme { // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default // This is to automatically make text and other content contrast to the background // correctly. 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 also work across composable functions Text("This Text uses the error color now") }
图 1. CompositionLocalExample
可组合项的预览。
在最后一个示例中,Material 可组合项内部使用了 CompositionLocal
实例。要访问 CompositionLocal
的当前值,请使用其 current
属性。在以下示例中,LocalContext
CompositionLocal
的当前 Context
值(在 Android 应用中常用)用于格式化文本:
@Composable fun FruitText(fruitSize: Int) { // Get `resources` from the current value of LocalContext val resources = LocalContext.current.resources val fruitText = remember(resources, fruitSize) { resources.getQuantityString(R.plurals.fruit_title, fruitSize) } Text(text = fruitText) }
创建自己的 CompositionLocal
CompositionLocal
是一个用于通过组合隐式向下传递数据的工具。
使用 CompositionLocal
的另一个关键信号是当参数是横切的并且中间实现层不应知道它存在时,因为让这些中间层知道会限制可组合项的实用性。例如,查询 Android 权限是由底层 CompositionLocal
提供的。媒体选择器可组合项可以添加新功能来访问设备上受权限保护的内容,而无需更改其 API 并要求媒体选择器的调用者了解此从环境中添加的上下文。
然而,CompositionLocal
并非总是最佳解决方案。我们不鼓励过度使用 CompositionLocal
,因为它带来一些缺点:
CompositionLocal
使可组合项的行为更难理解。由于它们创建隐式依赖项,使用它们的可组合项的调用者需要确保为每个 CompositionLocal
满足一个值。
此外,这种依赖项可能没有明确的真相来源,因为它可以在组合的任何部分发生变异。因此,当出现问题时,调试应用可能更具挑战性,因为您需要向上导航组合以查看 current
值是在哪里提供的。IDE 中的“查找用法”或 Compose 布局检查器等工具提供了足够的信息来缓解此问题。
决定是否使用 CompositionLocal
某些条件可以使 CompositionLocal
成为您用例的良好解决方案:
一个 CompositionLocal
应该有一个良好的默认值。如果没有默认值,您必须保证开发人员极难陷入未提供 CompositionLocal
值的情况。不提供默认值可能会在创建测试或预览使用该 CompositionLocal
的可组合项时导致问题和挫败感,因为它总是需要明确提供。
避免将 CompositionLocal
用于不被视为树作用域或子层次结构作用域的概念。CompositionLocal
在任何后代(而非少数后代)可能使用时才合理。
如果您的用例不符合这些要求,请在创建 CompositionLocal
之前查看要考虑的替代方案部分。
一个不好的实践是创建一个 CompositionLocal
来保存特定屏幕的 ViewModel
,以便该屏幕中的所有可组合项都可以获取 ViewModel
的引用来执行一些逻辑。这是一个不好的实践,因为特定 UI 树下的所有可组合项都不需要了解 ViewModel
。好的实践是遵循状态向下流动,事件向上流动的模式,只将它们所需的信息传递给可组合项。这种方法将使您的可组合项更具可重用性且更易于测试。
创建 CompositionLocal
有两种 API 可以创建 CompositionLocal
:
compositionLocalOf
:在重组期间更改提供的值只会使读取其current
值的内容失效。staticCompositionLocalOf
:与compositionLocalOf
不同,Compose 不会跟踪staticCompositionLocalOf
的读取。更改值会导致提供CompositionLocal
的整个content
lambda 被重新组合,而不仅仅是组合中读取current
值的地方。
如果提供给 CompositionLocal
的值极不可能更改或永远不会更改,请使用 staticCompositionLocalOf
以获得性能优势。
例如,应用的 Material Design 系统可能对可组合项通过阴影进行提升的方式有明确规定,以用于 UI 组件。由于应用的不同海拔高度应在整个 UI 树中传播,因此我们使用 CompositionLocal
。由于 CompositionLocal
的值根据系统主题有条件地派生,我们使用 compositionLocalOf
API:
// LocalElevations.kt file data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp) // Define a CompositionLocal global object with a default // This instance can be accessed by all composables in the app val LocalElevations = compositionLocalOf { Elevations() }
为 CompositionLocal
提供值
CompositionLocalProvider
可组合项将值绑定到给定层次结构中的 CompositionLocal
实例。要为 CompositionLocal
提供新值,请使用 provides
中缀函数,该函数将 CompositionLocal
键与 value
关联,如下所示:
// MyActivity.kt file class MyActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { // Calculate elevations based on the system theme val elevations = if (isSystemInDarkTheme()) { Elevations(card = 1.dp, default = 1.dp) } else { Elevations(card = 0.dp, default = 0.dp) } // Bind elevation as the value for LocalElevations CompositionLocalProvider(LocalElevations provides elevations) { // ... Content goes here ... // This part of Composition will see the `elevations` instance // when accessing LocalElevations.current } } } }
使用 CompositionLocal
CompositionLocal.current
返回最近的 CompositionLocalProvider
为该 CompositionLocal
提供的值。
@Composable fun SomeComposable() { // Access the globally defined LocalElevations variable to get the // current Elevations in this part of the Composition MyCard(elevation = LocalElevations.current.card) { // Content } }
要考虑的替代方案
对于某些用例,CompositionLocal
可能是一种过度的解决方案。如果您的用例不符合决定是否使用 CompositionLocal部分中指定的标准,那么其他解决方案可能更适合您的用例。
传递显式参数
明确可组合项的依赖关系是一个好习惯。我们建议您仅向可组合项传递它们所需的内容。为了鼓励解耦和可组合项的重用,每个可组合项应尽可能少地包含信息。
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel.data) } // Don't pass the whole object! Just what the descendant needs. // Also, don't pass the ViewModel as an implicit dependency using // a CompositionLocal. @Composable fun MyDescendant(myViewModel: MyViewModel) { /* ... */ } // Pass only what the descendant needs @Composable fun MyDescendant(data: DataToDisplay) { // Display data }
控制反转
另一种避免向可组合项传递不必要依赖项的方法是控制反转。即,由父级而非后代来执行某些逻辑。
请看以下示例,其中后代需要触发加载某些数据的请求:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... MyDescendant(myViewModel) } @Composable fun MyDescendant(myViewModel: MyViewModel) { Button(onClick = { myViewModel.loadData() }) { Text("Load data") } }
根据情况,MyDescendant
可能承担大量责任。此外,将 MyViewModel
作为依赖项传递会降低 MyDescendant
的可重用性,因为它们现在已耦合在一起。考虑另一种替代方案,它不将依赖项传递给后代,并使用控制反转原则,使祖先负责执行逻辑:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusableLoadDataButton( onLoadClick = { myViewModel.loadData() } ) } @Composable fun ReusableLoadDataButton(onLoadClick: () -> Unit) { Button(onClick = onLoadClick) { Text("Load data") } }
这种方法可能更适合某些用例,因为它将子级与其直接祖先解耦。祖先可组合项倾向于变得更复杂,以支持更灵活的低级可组合项。
同样,@Composable
内容 lambda 可以以相同的方式使用以获得相同的益处:
@Composable fun MyComposable(myViewModel: MyViewModel = viewModel()) { // ... ReusablePartOfTheScreen( content = { Button( onClick = { myViewModel.loadData() } ) { Text("Confirm") } } ) } @Composable fun ReusablePartOfTheScreen(content: @Composable () -> Unit) { Column { // ... content() } }
为您推荐
- 注意:禁用 JavaScript 时会显示链接文本
- Compose 中主题的剖析
- 在 Compose 中使用视图
- Jetpack Compose 版 Kotlin