今天大概会聊到的范围
- CompositionLocal
- CompositionLocalProvider
在上一篇研究 MaterialTheme
的时候,我们知道要使用 theme 内的颜色时,只需要使用 MaterialTheme.colors.xxx
就好了。但这个魔法是怎麽发生的呢?
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
中,其实个 property 都各只是一个 getter function,其实他们分别是 LocalColors
、LocalShapes
和 LocalTypography
。
咦?这个命名规则好像在哪里看过?
val context = LocalContext.current
val owner = LocalLifecycleOwner.current
仔细一看,发现这些 current 都为了实作同一个 property -- CompositionLocal.current
在 Compose 的结构中,Composable 要将某个资料传递给 child composable 时,通常会透过 state 参数往下传递。但是有时,某个比较里层的 composable 需要某个资料,但不想依赖中间每一层的 composable 都带着这个资料时,可以怎麽处理呢?
CompositionLocal
就是一个 "隐式的" 资料存放方式,在 CompositionLocal
所控制的 Scope 以下的树状结构,都可以取得同一组 CompositionLocal
与其中的资料。
@Composable
fun Screen() {
MaterialTheme {
// ... 这里可能有很多 composable
SomeComp()
}
}
@Composable
fun SomeComp(){
SomeInnerComp()
}
@Composable
fun SomeInnerComp() {
val color = MaterialTheme.color.primaryColor // 即便传了好几层还是可以取得 color
}
让我们再次看看 MaterialTheme
的 source code
@Composable
fun MaterialTheme(
colors: Colors = MaterialTheme.colors,
typography: Typography = MaterialTheme.typography,
shapes: Shapes = MaterialTheme.shapes,
content: @Composable () -> Unit
) {
val rememberedColors = remember {
// Explicitly creating a new object here so we don't mutate the initial [colors]
// provided, and overwrite the values set in it.
colors.copy()
}.apply { updateColorsFrom(colors) }
val rippleIndication = rememberRipple()
val selectionColors = rememberTextSelectionColors(rememberedColors)
CompositionLocalProvider( // <--- 1.
LocalColors provides rememberedColors, // <--- 2.
LocalContentAlpha provides ContentAlpha.high,
LocalIndication provides rippleIndication,
LocalRippleTheme provides MaterialRippleTheme,
LocalShapes provides shapes,
LocalTextSelectionColors provides selectionColors,
LocalTypography provides typography
) { // <--- 3.
ProvideTextStyle(value = typography.body1, content = content)
}
}
MaterialTheme
中,建立了一个 CompositionLocalProvider
[1]。在建立 CompositionLocalProvider
时,会需要透过 "provides" 将资料设定到 LocalXXX 内 [2]。这些 LocalXXX 各自都是自己一个 ProvidableCompositionLocal
(继承於 CompositionLocal
)。ProvidableCompositionLocal
继承於 CompositionLocal
,并且可以透过 provides function 提供资料。最後在提供要包在这个 CompositionLocalProvider
中的 content [3]。
MaterialTheme
中包的 content 是一个 ProvideTextStyle
。ProvideTextStyle
中又有另一个 CompositionLocalProvider
:
@Composable
fun ProvideTextStyle(value: TextStyle, content: @Composable () -> Unit) {
val mergedStyle = LocalTextStyle.current.merge(value)
CompositionLocalProvider(LocalTextStyle provides mergedStyle, content = content)
}
这里要提到另一个概念,CompositionLocal
是有范围性的。CompositionLocalProvider
所包装的 content 与其中所包中的每一层 composable 都可以取得对应的 LocalXXX。
CompositionLocal
)建立 CompositionLocal
的方法有两个:compositionLocalOf()
和 stataicCompositionLocalOf()
。
val LocalData = compositionLocalOf {
// factory fucntion to create initial data
}
产生完 LocalData 後,再建立 CompositionLocalProvider
并提供资料
CompositionLocalProvider(
LocalData provides <data>
) {
UIComposable()
}
在 provides 资料时,可以 provide 一般资料之外,也可以提供 state。当 provides 时提供的 state 改变时,就会触发 recompistion。
compositionLocalOf
:当资料改变时,有使用到该 LocalXXX 的 Composable 会触发 recompositionstataicCompositionLocalOf
:整个 CompositionLocal
所包覆的 composable 和整个 compose tree 都会被 recomposeCompositionLocalProvider
除了可以将资料放进自定义的 CompositionLocal
之外,也可以用来覆写上层 CompistionLocalProvider
所提供的参数。
@Composable
fun CompositionLocalExample() {
MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Medium value provided for LocalContentAlpha")
Text("This Text also uses the medium value")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
}
compose 官方文件所提供的范例
虽然 CompositionLocal
可以很方便的提供资料给好几层之外的 Composable 资料,但同时也很容易被滥用。因为 CompositionLocal
是隐性的传递,无法明确知道资料的设定方与取得方分别在哪。所以在使用 CompositionLocal
时可以停看听:
CompositionLocal
是否有预设值?是否一定会有值被写入?CompositionLocal
是否真的可能被整个 View Tree 中的“任何”一个人用到?如果不符合,也许可以选用别的传递方式。
官方文件有提反面例子:我们是否能用 CompositionLocal
存放 ViewModel
?
我的第一直觉是可以,因为 ViewModel
的确会影响到整个 tree 的 UI。
但是仔细想想,其实不是每一个按钮、每一个 Text 都需要知道 ViewModel
与其中的各种资料,所以透过 CompostionLocal
将 ViewModel
存放起来并不是一个好方法。
取而代之,可以考虑将资料透过参数传递,但提供 default 值:
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
MyDescendant(myViewModel.data)
}
发现 CompositionLocal
其实很常在大大小小的地方出现,只是通常都是别人准备好的 CompostionLocal
。实际研究後发现它不仅是一个好用的工具,同时也是一个非常重要且有趣的主题!
Reference:
>>: [Day22] Scrum失败经验谈 – 承认就是陨石吧!
今天来谈谈修饰子(Modifier)。 修饰子我觉得可以分为三大类,第一种就是封装用的修饰子,第二种...
目前的 E-ink 设备,6寸,7.8寸,一直到 10 寸,13 寸都有,除了6 寸有点太小,其他尺...
在本篇, 我们来看一下使用seldon完成部署之後, 在k8s上会产生哪些资源 建立在k8s上的se...
本篇同步发布於个人Blog: [PoEAA] Domain Logic Pattern - Tabl...
有鉴於Junior Programmer: Manage scene flow and data课程...