CompositionLocal
Compose 中的“隐式向下传递数据”
要在 Jetpack Compose 里使用主题,只需保证 Compose 可组合函数在 MaterialTheme 作用域内被调用,这样我们就可以在组件中使用 MaterialTheme.colors.primary
等属性,从而读取主题信息。
@Composable
fun PrimaryBox() {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.primary)
.size(50.dp)
)
}
@Preview
@Composable
fun PrimaryBoxPreview() {
MaterialTheme(colorScheme = lightColorScheme(primary = YELLOW)) {
Row {
PrimaryBox()
MaterialTheme(colorScheme = lightColorScheme(primary = BLUE)) {
Column {
PrimaryBox()
PrimaryBox()
}
}
}
}
}
写起来简单且合理对吧,但你有没有想过,这种在 Compose 中“隐式向下传递数据”背后的实现原理?
什么东西?...脑子有点懵,别说原理了,“隐式向下传递数据”指的是???
没关系,让我们一步一步慢慢捋捋。
在 Compose 中,每个组件都是一个 Composable 函数,它们被调用并生成一个个 LayoutNode,这些 LayoutNode 节点共同组成一颗 UI 树。
通常情况下,数据以函数参数的形式在 UI 树中从上往下流动。 举个例子,假设有一个新闻列表页面:
@Composable
fun NewsPage(newsList: List<News>) {
LazyColumn {
items(newsList) { news ->
NewsItem(news.title, news.category, news.coverUrl, news.readCount)
}
}
}
@Composable
fun NewsItem(title: String, category: String, coverUrl: String, readCount: Int) {
Row {
Column {
Text(category) // 分类
Text(title) // 标题
}
Text(readCount) // 阅读量
AsyncImage(model = coverUrl) // 封面
}
}
界面数据以函数参数的形式,显式地在 UI 树中从上往下流动:
主题也是数据,它又是怎么在 UI 树中流动的呢?
我们再回头看一下最初的代码:
@Composable
fun PrimaryBox() {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.primary)
.size(50.dp)
)
}
@Preview
@Composable
fun PrimaryBoxPreview() {
MaterialTheme(colorScheme = lightColorScheme(primary = YELLOW)) {
Row {
PrimaryBox()
MaterialTheme(colorScheme = lightColorScheme(primary = BLUE)) {
Column {
PrimaryBox()
PrimaryBox()
}
}
}
}
}
虽然我们没有显式地将主题声明为各个组件的参数,使其在 UI 树中向下流动,但是从上图不难看出,MaterialTheme 会将主题数据隐式地向子树传递。
注意,这和全局变量不同,因为它是以树为作用范围,而不是全局的。
这是怎么做到的呢?说了半天终于要请出今天的主角了——CompositionLocal,这是一种通过组合(Composition)隐式向下传递数据的工具。对于那些使用非常广泛或频繁的数据,比如主题、上下文 Context、语言等,要将其作为函数参数显式传递给 UI 树的每个组件非常麻烦,同时我们也不希望将这些数据作为全局变量,因为它不够灵活,作用范围不能局限在 UI 树中的某个子树,这个时候 CompositionLocal 就派上用场了。
百闻不如一见,我决定用一个简单的例子来展示一下 CompositionLocal。
创建自己的 CompositionLocal
现在假设我们要创建一个最简化的主题,里面只有一个颜色 colorImportant
,我们要在 UI 树中隐式向下传播这个颜色。
1. 创建 CompositionLocal 对象
// 1.调用 compositionLocalOf 创建一个 ProvidableCompositionLocal 对象,并提供默认值
val LocalColorImportant: ProvidableCompositionLocal<Color> =
compositionLocalOf { /* default value */ RED }
首先,调用 compositionLocalOf
方法创建一个 CompositionLocal 对象,通常情况下,我们会将变量命名为 "Local...",并且定义为顶级属性,因为我们希望所有组件都能访问到这个颜色。
在创建 CompositionLocal 对象的时候,我们可以提供一个默认值,这个默认值有什么用呢?回想一下上面 MaterialTheme 的例子,在 MaterialTheme 作用域内,所有组件都可以使用 MaterialTheme.colorScheme.primary
访问主题的 Primary 颜色,那如果在 MaterialTheme 作用域外,MaterialTheme.colorScheme.primary
会报错吗?还是会返回默认主题的 Primary 颜色?
@Preview
fun PrimaryBoxPreview() {
- MaterialTheme(colorScheme = lightColorScheme(primary = YELLOW)) {
Box(
modifier = Modifier
.background(MaterialTheme.colorScheme.primary)
.size(50.dp)
)
- }
}
答案是不会报错,作为使用者,你不自行提供自定义主题,读取颜色时就会获取到 Material Design 默认主题的颜色,这就是 CompositionLocal 默认值的作用。
当然,现在你作为 CompositionLocal 的提供者/设计者,可以根据实际需求自行决定是否提供默认值,如果你希望使用者必须自行为 CompositionLocal 提供值,那么可以不提供默认值,而是简单的抛错误:compositionLocalOf<T> { error("No default value provided") }
2. 提供并传递 CompositionLocal 值
@Preview
@Composable
fun ImportantBoxPreview() {
Row {
ImportantBox()
// 2.使用 CompositionLocalProvider 向子组件传递 CompositionLocal 值
CompositionLocalProvider(LocalColorImportant.provides(BLUE)) {
ImportantBox()
CompositionLocalProvider(
LocalColorImportant provides YELLOW,
// LocalXxx provides ...
) {
ImportantBox()
}
}
}
}
有了 CompositionLocal 对象之后,我们就可以调用 CompositionLocalProvider()
向子组件传递新的 CompositionLocal 值了。
@Composable
fun CompositionLocalProvider(
vararg values: ProvidedValue<*>,
content: @Composable () -> Unit
)
CompositionLocalProvider
有一个参数 values
,很明显它就是隐式向下传播的数据,由于我们可能会一次性向下传递多个数据,所以 values
是一个可变参数,它的类型是 ProvidedValue<*>
,我们可以调用 ProvidableCompositionLocal
的中缀函数 provides
来创建 ProvidedValue
对象。
CompositionLocalProvider(LocalXXX provides 具体值) {
/* content */
}
现在我们已经完成了 CompositionLocal 数据的隐式向下传递,CompositionLocalProvider 作用域中的所有子组件都可以访问到我们提供的值。
3. 读取 CompositionLocal 值
读取 CompositionLocal 的值非常简单,只需要使用 CompositionLocal 的 current
属性即可,它会返回 UI 树中最近父级 CompositionLocalProvider 提供的值,若没有找到,则返回默认值。
@Composable
fun ImportantBox() {
Box(
modifier = Modifier
// 3.通过 CompositionLocal 的 current 属性获取值
.background(LocalColorImportant.current)
.size(50.dp)
)
}
完整代码
// 1.调用 compositionLocalOf 创建一个 ProvidableCompositionLocal 对象,并提供默认值
val LocalColorImportant: ProvidableCompositionLocal<Color> =
compositionLocalOf { /* default value */ RED }
@Preview
@Composable
fun ImportantBoxPreview() {
Row {
ImportantBox()
// 2.使用 CompositionLocalProvider 向子组件传递 CompositionLocal 值
CompositionLocalProvider(LocalColorImportant.provides(BLUE)) {
ImportantBox()
CompositionLocalProvider(LocalColorImportant provides YELLOW) {
ImportantBox()
}
}
}
}
@Composable
fun ImportantBox() {
Box(
modifier = Modifier
// 3.通过 CompositionLocal 的 current 属性获取值
.background(LocalColorImportant.current)
.size(50.dp)
)
}
你可以简单的把这个过程看作是:在 UI 树中的某个节点,存了一个或多个键值对形式的数据,这些数据会顺着子树往下流,在下游的某个子节点可以通过“键”拿到上游最近往这个键存进去的数据。CompositionLocal 就相当于键,而 CompositionLocalProvider(LocalXXX provides ...)
相当于存数据,LocalXXX.current
相当于取数据。
动态更新 CompositionLocal 值
我们还可以可以动态更新 CompositionLocal 的值,只需要在为 CompositionLocal 提供值的时候,提供一个 State 对象,后续当 State 对象发生变化时,读取了 CompositionLocal.current
的地方会自动进行重组。
val LocalColorImportant = compositionLocalOf { RED }
@Preview
@Composable
fun ImportantButtonPreview() {
var importantColor by remember { mutableStateOf(BLUE) }
// 给 CompositionLocal 提供传递一个 State 对象
CompositionLocalProvider(LocalColorImportant provides importantColor) {
Button(
colors = ButtonDefaults.buttonColors(containerColor = LocalColorImportant.current),
onClick = { importantColor = randomColor() } // 更新 State 对象
) { Text(text = "Random Important Color") }
}
}
利用这个特性可以轻松实现主题预览/换肤等功能。
compositionLocalOf
VS staticCompositionLocalOf
除了 compositionLocalOf
,还有一个非常类似的函数 staticCompositionLocalOf
,同样可以创建一个 ProvidableCompositionLocal 对象,它们有什么区别呢?
前面提到,使用 compositionLocalOf
,当 CompositionLocal 值发生变化时,只有读取了 CompositionLocal.current
的地方会自动进行重组。而如果使用 staticCompositionLocalOf
,当 CompositionLocal 值发生变化时,整个 content 范围都会进行重组,无论组件是否读取了 CompositionLocal.current
。
我们来试验一下:
val LocalInt = compositionLocalOf<Int> { error("No default value provided for LocalInt") }
@Composable
fun CompositionLocalTest() {
var counter by remember { mutableIntStateOf(0) }
Button(onClick = { counter++ }) {
Text(text = "+1")
}
CompositionLocalProvider(LocalInt provides counter) {
Parent()
}
}
@Composable
fun Parent() {
Log.e("CompositionLocal", "Parent: ${LocalInt.current}")
Child()
}
@Composable
fun Child() {
Log.e("CompositionLocal", "Child")
}
这里创建一个简单的 LocalInt
,点击时将提供的值 +1。可组合项 Parent
调用了 Child
,前者在打印日志时读取了 LocalInt.current
。
通过 logcat 观察到,每次点击触发 LocalInt
更新,读取了 LocalInt.current
的可组合项 Parent
会发生重组,没有读取 LocalInt.current
的子节点 Child
则被跳过。
现在把 compositionLocalOf
换成 staticCompositionLocalOf
再试试:
- val LocalInt = compositionLocalOf<Int> { error("No default value provided for LocalInt") }
+ val LocalInt = staticCompositionLocalOf<Int> { error("No default value provided for LocalInt") }
由于使用了 staticCompositionLocalOf
,当 LocalInt
改变时,content 范围内的所有节点都会被重组(无论是否读取了 LocalInt
)。
所以,到底应该在什么时候应该使用 staticCompositionLocalOf
呢?用它有什么好处吗?按官方的说法,当 CompositionLocal 的值极少发生改变或根本不会改变时,就应该使用 staticCompositionLocalOf
,以此获得更好的性能。比如,我想设计一个 LocalApplicationContext,很明显,它在整个 app 的生命周期内都不会发生改变,那么我们就应该使用 staticCompositionLocalOf
:
val LocalApplicationContext = staticCompositionLocalOf<Context> { ... }
小结
CompositionLocal 是 Compose 中的一种数据传递机制,它可以让我们在 UI 树中隐式向下传递数据,而不需要显式地将数据作为参数传递给每个组件。使用方法非常简单,首先使用 compositionLocalOf
创建 CompositionLocal 对象,然后用 CompositionLocalProvider 为 CompositionLocal 提供具体的值,后续使用 CompositionLocal.current
读取值就 OK 了。当 CompositionLocal 很少改变或不会改变,则应该用 staticCompositionLocalOf
来优化性能。
另外,其实官方已经为我们提供许多常用的 CompositionLocal,比如 LocalContext
、LocalDensity
、LocalLifecycleOwner
等,大家可以自行探索。
参考