Jetpack Compose 中的 CompositionLocal
Posted 川峰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Jetpack Compose 中的 CompositionLocal相关的知识,希望对你有一定的参考价值。
要在可组合函数之间共享数据时,可以通过参数传递显式地调用,这通常是最简单和最好的方式。
但随着参数越来越多,组件也越来越多,并且有些数据还需要保持私有性,这时这种方式就会显得很繁琐臃肿,难以维护。 对于这些情况,Compose 提供了CompositionLocals
来作为一种隐式方式在组合函数之间传递数据。说白了就是在 Composable 树中提供的一种共享数据的方式(例如主题配置)。
MaterialTheme 是如何实现的
需要注意的是,此时传入的 content
参数其实是声明在 Theme
中的自定义布局系统,其类型是一个带有 Composable
注解的 lambda
。
我们所关注的 colors
被 remember
修饰后赋值为 rememberedColors
。如果 MaterialTheme
这个 Composable
发生 recompose
时便会检查 colors
是否发生了改变从而决定更新。
接下来使用 CompositionLocalProvider
方法,通过中缀表达式 providers
将 rememberedColors
提供给了 LocalColors
。让我们回到自己的 Composable
中,看看我们是如何通过 MaterialTheme
获取到当前主题配色的。
@Composable
fun MyCard(text: String)
Box(
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
)
Text(text = text, color = MaterialTheme.colors.primary)
这里使用的 MaterialTheme.colors
实际上被定义在 MaterialTheme object
单例对象中:
object MaterialTheme
val colors: Colors
@Composable
@ReadOnlyComposable
get() = LocalColors.current
val typography: Typography
@Composable
@ReadOnlyComposable
get() = LocalTypography.current
val shapes: Shapes
@Composable
@ReadOnlyComposable
get() = LocalShapes.current
MaterialTheme
类单例的 colors
属性,间接使用了 LocalColors
总的来说,我们在自定义 Theme
使用的是 MaterialTheme
函数为 LocalColors
赋值,而在获取时使用的是 MaterialTheme
类单例,间接从 LocalColors
中获取到值。那 LocalColors
又是什么呢?
internal val LocalColors = staticCompositionLocalOf lightColors()
实际上它是一个 CompositionLocal
,其初始值是 lightColors()
返回的 colors 配置。
MaterialTheme
方法中通过 CompositionLocalProvider
方法为我们的自定义视图 Composable
提供了一些 CompositionLocal
,包含了所有的主题配置信息。
CompositionLocal 介绍
CompositionLocals
本质上是分层的。当CompositionLocal
的值需要被限定于组合的特定子层次结构时,它们是有意义的。
CompositionLocals
可以被限定在以某个 Composable
作为根结点的子树中,其默认会向下传递的,当然当前子树中的某个 Composable
可以对该 CompositionLocals
进行覆盖,从而使得新值会在这个 Composable
中继续向下传递。
总的来说,CompositionLocal 它有以下特性:
- 具备函数穿透功能的局部变量,不需要显示的传递的函数参数,
- 多用于提供:上下文/主题 等,方便透传
- 它的着重点在于提供某种上下文,如果是某个函数需要某个参数多数情况应该使用函数参数直接传
要使用 CompositionLocal
必须创建一个 CompositionLocal 实例,消费者可以静态地引用该实例。CompositionLocal 实例本身不保存任何数据,可以将其视为在组合树中向下传递的数据的类型安全标识符。
要创建一个 CompositionLocal 的实例通过调用 compositionLocalOf
方法来实现:
import androidx.compose.runtime.compositionLocalOf
var LocalString = compositionLocalOf "Jetpack Compose"
compositionLocalOf
后面的 lambda 中返回的是一个默认值,这个 lambda 其实是一个工厂函数,其中还可以配置没有提供值的情况下的警告信息,以提醒使用者:
val LocalBackground = compositionLocalOf<Color> error("LocalBackground没有提供值")
compositionLocalOf
的返回值是一个 ProvidableCompositionLocal
对象(它继承了 CompositionLocal
类)。
然后,在 Composable 树的某个地方,我们可以使用 CompositionLocalProvider
方法为 CompositionLocal
提供一个值。通常情况下位于 Composable
树的根部,但也可以位于任何位置,还可以在多个位置使用,以覆盖子树能够获取到的值。
val LocalUserName = compositionLocalOf<String> ""
class CompositionLocalProviderActivity : ComponentActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContent
CompositionLocalProvider(LocalUserName provides "Jetpack Compose")
User()
@Composable
fun User()
Column
Text(text = LocalUserName.current) // 通过current当前用户名
比如可以通过这种方式来为 Composable 提供当前 Activity 的实例:
val LocalActivity = compositionLocalOf<Activity> error("LocalActivity没有提供值")
class CompositionLocalProviderActivity : ComponentActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContent
CompositionLocalProvider(LocalActivity provides this)
User() // 范围内都可以使用LocalActivity变量访问当前Activity对象
其中 provides
是 ProvidableCompositionLocal
抽象类中定义的一个 infix
中缀表达式:
abstract class ProvidableCompositionLocal<T> internal constructor(defaultFactory: () -> T) : CompositionLocal<T> (defaultFactory)
infix fun provides(value: T) = ProvidedValue(this, value, true)
以下示例是在任意子 Composable
中嵌套使用 CompositionLocalProvider
:
// Top Level
val LocalBackground = compositionLocalOf Color.Magenta
class CompositionLocalProviderActivity : ComponentActivity()
override fun onCreate(savedInstanceState: Bundle?)
super.onCreate(savedInstanceState)
setContent
Column
SomethingtWithBackground() // 默认背景色
CompositionLocalProvider(LocalBackground provides Color.Red )
SomethingtWithBackground() // 红色
CompositionLocalProvider(LocalBackground provides Color.Green)
SomethingtWithBackground() // 绿色
CompositionLocalProvider(LocalBackground provides Color.Blue)
SomethingtWithBackground() // 蓝色
SomethingtWithBackground() // 绿色
SomethingtWithBackground() // 红色
SomethingtWithBackground() // 默认背景色
@Composable
fun SomethingtWithBackground()
Box(
modifier = Modifier
.fillMaxWidth()
.background(LocalBackground.current)
.padding(20.dp)
)
Text(text = "我是有背景的人!", color = Color.White, fontSize = 22.sp)
可以看到在不同嵌套层级范围内相同的 CompositionLocal
获取到的值可以不一样,也就是说可以在某个子树范围中被覆盖,但是出了这个子树范围后,还是原来的值。
注意,CompositionLocalProvider
中的第一个参数是一个可变参数,也就是可以提供多个值,例如:
// Top Level
val LocalUserName = compositionLocalOf "张三"
Column
SomethingtWithBackground()
CompositionLocalProvider(
LocalBackground provides Color.Red,
LocalUserName provides "李四"
)
SomethingtWithBackground()
CompositionLocalProvider(LocalBackground provides Color.Green)
SomethingtWithBackground()
CompositionLocalProvider(
LocalBackground provides Color.Blue,
LocalUserName provides "小明"
)
SomethingtWithBackground()
SomethingtWithBackground()
SomethingtWithBackground()
SomethingtWithBackground()
@Composable
fun SomethingtWithBackground()
Box(
modifier = Modifier
.fillMaxWidth()
.background(LocalBackground.current)
.padding(20.dp)
)
Text(text = LocalUserName.current, color = Color.White, fontSize = 22.sp)
compositionLocalOf 与 staticCompositionLocalOf 区别
当需要创建 CompositionLocal
时,除了可以使用 compositionLocalOf
方法,在 Compose 中还有一个 staticCompositionLocalOf
方法,那么这两者有什么区别呢?
-
当我们选择使用
staticCompositionLocalOf
时,实际上创建了个StaticProvidableCompositionLocal
实例,与compositionLocalOf
不同,编译器不会跟踪staticCompositionLocalOf
的读取,一旦它的值改变时,它所提供范围内的所有内容都会重组,而不仅仅是组合中使用局部值的地方。也就是说它不会进行智能重组,每次改变值时都是强制所有人重组。 -
如果我们选择使用
compositionLocalOf
,实际上创建了个DynamicProvidableCompositionLocal
实例,当其所提供的值改变时,仅会导致它所提供范围内的依赖当前CompositionLocal
的那个 Composable 触发重组。
下面是一个分别使用 staticCompositionLocalOf
与 compositionLocalOf
创建 CompositionLocal
的对照效果示例:
val LocalCounter = compositionLocalOf 0
val LocalCounterStatic = staticCompositionLocalOf 0
@Composable
fun CompositionLocalExample()
var counter by remember mutableStateOf(0)
Column(horizontalAlignment = Alignment.CenterHorizontally)
Button(onClick = counter++ )
Text(text = "change LocalCounter")
CompositionLocalProvider(LocalCounter provides counter)
ThreeBox("CompositionLocal")
Text("counter: $LocalCounter.current", color = Color.White,
modifier = Modifier.background(getRandomColor()).padding(5.dp))
Spacer(Modifier.height(10.dp))
CompositionLocalProvider(LocalCounterStatic provides counter)
ThreeBox("StaticCompositionLocal")
Text("counter: $LocalCounterStatic.current", color = Color.White,
modifier = Modifier.background(getRandomColor()).padding(5.dp))
@Composable
fun ThreeBox(type: String, content: @Composable () -> Unit)
Box(
modifier = Modifier
.size(300.dp)
.background(getRandomColor())
)
Box(modifier = Modifier
.padding(50.dp)
.fillMaxSize()
.background(getRandomColor())
)
Box(modifier = Modifier
.padding(50.dp)
.fillMaxSize()
.background(getRandomColor()),
contentAlignment = Alignment.Center
)
content()
Text(type, color = Color.White, fontSize = 18.sp,
modifier = Modifier.padding(top=15.dp).align(Alignment.TopCenter))
运行效果:
在上面代码中,三个 Box
及最里面的 Text
组件都设置了随机背景色,这样一旦它们发生重组,我们就能观察到。可以看到,点击修改 counter
状态值时,staticCompositionLocalOf
与 compositionLocalOf
创建的本地共享变量有着明显不同的表现:前者强制其范围内的所有组件重组(不管是否从CompositionLocal中读取值),后者仅重组了从其读取值的组件。
Composable 中的常见 CompositionLocals 创建流程分析
通过前面部分的对比示例可见,使用 staticCompositionLocalOf
主要目的是为了某种全局性的配置,例如开头分析的 MaterialTheme
中的主题颜色都是通过 staticCompositionLocalOf
实现的,因为这些主题是整个应用中共有的属性,需要做到一改全改的效果。此外,我们常用的 LocalContext
等带Localxxx
前缀的实现方式都是 staticCompositionLocalOf
。
当我们在 Activity、Fragment 或 ComposeView 调用 setContent
的时候,会创建 Composition
(组合)对象,在创建该对象时会先创建一个 AndroidComposeView
来作为 LayoutNode 组合树的跟节点的 Owner,然后这个 AndroidComposeView
被附加到 Android 的 View
视图层次结构中,以便可以根据需要执行 invalidate
操作(作为与Compose 视图树的集成点)。
然后会创建一个 WrappedComposition 对象,WrappedComposition 是一个装饰器,它知道如何将 Composition 链接到一个 AndroidComposeView,以便将其直接连接到 Android View 系统。它启动受控效果来跟踪诸如键盘可见性更改或 accessibility 之类的内容,并将关于 Android Context 的信息以 CompositionLocals
的形式传输暴露给 Composition 。
WrappedComposition 设置这些 CompositionLocals
具体是通过调用 ProvideAndroidCompositionLocals
方法:
然后在 ProvideAndroidCompositionLocals
方法中,我们就能够看到例如:Context
本身、配置、当前LifecycleOwner
、当前savedStateRegistryOwner
或Owner
的View
等等,这些是如何被 CompositionLocalProvider
提供给 LocalXXX
类的:
@Composable
@OptIn(ExperimentalComposeUiApi::class)
internal fun ProvideAndroidCompositionLocals(owner: AndroidComposeView, content: @Composable () -> Unit)
val view = owner
val context = view.context
var configuration by remember
mutableStateOf(
context.resources.configuration,
neverEqualPolicy()
)
owner.configurationChangeObserver = configuration = it
val uriHandler = remember AndroidUriHandler(context)
val viewTreeOwners = owner.viewTreeOwners ?: throw IllegalStateException(
"Called when the ViewTreeOwnersAvailability is not yet in Available state"
)
val saveableStateRegistry = remember
DisposableSaveableStateRegistry(view, viewTreeOwners.savedStateRegistryOwner)
DisposableEffect(Unit)
onDispose
saveableStateRegistry.dispose()
val imageVectorCache = obtainImageVectorCache(context, configuration)
CompositionLocalProvider(
LocalConfiguration provides configuration,
LocalContext provides context,
LocalLifecycleOwner provides viewTreeOwners.lifecycleOwner,
LocalSavedStateRegistryOwner provides viewTreeOwners.savedStateRegistryOwner,
LocalSaveableStateRegistry provides saveableStateRegistry,
LocalView provides owner.view,
LocalImageVectorCache provides imageVectorCache
)
ProvideCommonCompositionLocals(
owner = owner,
uriHandler = uriHandler,
content = content
)
这就是为何这些内容在我们所有的 Composable
函数当中是隐式可用的。
CompositionLocal 的替代方案
某些场景下,CompositionLocal 可能不合适,甚至过度使用。此时可采取如下方案:
-
显式参数:在极简单逻辑情况,应尽量使用显示参数传递,且只传递有效参数,避免造成参数过多。
-
控制反转:另一种避免参数过多或无效参数的方法就是控制反转。一些逻辑可以不在子级页面进行,而应该转移到父级页面来进行。
例如下面的例子中,在子级页面使用了
viewModel
调用loadData
@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")
在某些场景下控制反转可以将子级脱离出来,达到高度复用,可以更灵活。同样,可以用 lambda 表达式来实现:
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel())
// ...
ReusablePartOfTheScreen(
content =
Button(
onClick =
myViewModel.loadData()
)
Text("Confirm")
)
@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit)
Column
// ...
content()
以上是关于Jetpack Compose 中的 CompositionLocal的主要内容,如果未能解决你的问题,请参考以下文章