D23/ MaterialTheme 怎麽运作的? - CompositionLocal

今天大概会聊到的范围

  • 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,其实他们分别是 LocalColorsLocalShapesLocalTypography

咦?这个命名规则好像在哪里看过?

val context = LocalContext.current
val owner = LocalLifecycleOwner.current

仔细一看,发现这些 current 都为了实作同一个 property -- CompositionLocal.current

什麽是 CompositionLocal?

在 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 是一个 ProvideTextStyleProvideTextStyle 中又有另一个 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。

定义一个 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 会触发 recomposition
  • stataicCompositionLocalOf:整个 CompositionLocal 所包覆的 composable 和整个 compose tree 都会被 recompose

覆写 CompositionLocal.current

CompositionLocalProvider 除了可以将资料放进自定义的 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 与其中的各种资料,所以透过 CompostionLocalViewModel 存放起来并不是一个好方法。

取而代之,可以考虑将资料透过参数传递,但提供 default 值:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel.data)
}

发现 CompositionLocal 其实很常在大大小小的地方出现,只是通常都是别人准备好的 CompostionLocal。实际研究後发现它不仅是一个好用的工具,同时也是一个非常重要且有趣的主题!


Reference:


<<:  Day 22 来写一个简单e2e测试

>>:  [Day22] Scrum失败经验谈 – 承认就是陨石吧!

Day08:别为了钱而放弃权力

今天来谈谈修饰子(Modifier)。 修饰子我觉得可以分为三大类,第一种就是封装用的修饰子,第二种...

电子书阅读器上的浏览器 [Day12] 桌面模式

目前的 E-ink 设备,6寸,7.8寸,一直到 10 寸,13 寸都有,除了6 寸有点太小,其他尺...

细看seldon core所部署出来的POD在做什麽

在本篇, 我们来看一下使用seldon完成部署之後, 在k8s上会产生哪些资源 建立在k8s上的se...

[PoEAA] Domain Logic Pattern - Table Module

本篇同步发布於个人Blog: [PoEAA] Domain Logic Pattern - Tabl...

Day 11. 来学习如何切换场景!2

有鉴於Junior Programmer: Manage scene flow and data课程...