Jetpack Compose (五) ——— Composition Local

946 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

一、显式传参与隐式传参

在函数间传递参数的代码通常是这样的:

fun Layout() {
    val color = Color.BLACK
    Button(color)
}

fun Button(color: Int) {
    Log.d("~~~", "get color: $color")
}

在 Layout 函数中,有一个 color 变量,为了将其传递给 Button 函数,我们给 Button 函数添加了一个参数,然后将 color 作为参数传入。这就叫显式传参,我们可以看到参数传递过程。

那么何谓隐式传参呢?代码如下:

var color = 0

fun Layout() {
    color = Color.BLACK
    Button()
}

fun Button() {
    Log.d("~~~", "get color: $color")
}

隐式传参类似于:定义一个全局变量,在 Layout 中修改此变量,然后 Button() 函数就可以直接使用 Layout 中修改后的这个变量了。

Button 函数不需要添加参数,也能获取到 color 参数,我们没有看到参数传递过程,但参数确实传递到 Button 函数中了。

看到这么一本正经的胡说八道,读者可能想要拔刀了,这哪里叫传参?这不是使用全局字段吗?

大哥息怒,注意我这里的措辞:类似于。真正的隐式传参和全局变量是有区别的,隐式传参应该这么写:

var color = 0

fun Layout() {
    provide(Color.BLACK, {
        Button()
    })
}

fun provide(value: Int, content: (() -> Unit)) {
    val originColor = color
    color = value
    content()
    color = originColor
}

fun Button() {
    Log.d("~~~", "get color: $color")
}

在这份代码中,我们编写了一个 provide 函数,它接收两个参数,一个颜色值,一个代码块。

在执行代码块之前,先将全局变量 color 的初始值保存起来,然后再将其修改成传进来的颜色值,再执行代码块。

执行完代码块后,再将全局变量 color 改回初始值。

所以在执行代码块时,全局变量 color 等于传进来的颜色值,在 provide 中的 Button() 函数中可以拿到这个颜色值,但 provide 函数之外的其他地方则不会受影响:

fun Layout() {
    provide(Color.BLACK, {
        Button()
    })
    // 这里不会取到 Color.BLACK
    Button()
}

这就是隐式传参。

此时一位暴躁的读者忍无可忍,挥起手中的大刀,大叫到:胡说,这还是全局变量!

我:这是全局变量吗?

读者:是。

我:但它会影响其他地方吗?

读者:不会。

我:全局变量不是会影响其他地方吗?

读者:对。

我:那是不是说明这不是全局变量?

读者:嗯...不对,它不影响其他地方只是因为它用完改回了初始值。

我:这就是隐式传参。

读者:这不是隐式传参,这是全局变量!

我:这既是隐式传参,又是全局变量。

读者:所以隐式传参是一种特殊的全局变量?

我:对。

读者收刀入鞘:咋不早说。

了解了隐式传参,就很容易理解 Composition Local 了。

二、Composition Local

通常 CompositionLocal 的使用场景如下:

CompositionLocalProvider(LocalContentAlpha.provides(ContentAlpha.medium)) {
    Text("Hello")
}

在这个例子中,通过 CompositionLocalProvider 进行隐式传参,将 LocalContentAlpha 设置为 ContentAlpha.medium。

LocalContentAlpha 这个参数表示内容的透明度,默认为 1.0,ContentAlpha.medium 表示 中等透明度,从源码中可以看出这个值是 0.74 或 0.60:

val medium: Float
    @Composable
    get() = contentAlpha(
        highContrastAlpha = HighContrastContentAlpha.medium,
        lowContrastAlpha = LowContrastContentAlpha.medium
    )
    
private object HighContrastContentAlpha {
    const val high: Float = 1.00f
    const val medium: Float = 0.74f
    const val disabled: Float = 0.38f
}

private object LowContrastContentAlpha {
    const val high: Float = 0.87f
    const val medium: Float = 0.60f
    const val disabled: Float = 0.38f
}

所以这段代码的意思是,通过隐式传参将透明度设置为中等透明度。

在这个 CompositionLocalProvider 作用域中的所有控件的透明度都会变成中等透明度,而作用域外的所有控件则不会被影响。

做个简单的测试:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("Hello1")
        CompositionLocalProvider(LocalContentAlpha.provides(ContentAlpha.medium)) {
            Text("Hello2")
        }
        Text("Hello3")
    }
}

这个控件的运行效果如下:

medium

可以看出,在 CompositionLocalProvider 作用域内的控件显示的 Hello2 是中等透明度的。

三、自定义 Composition Local

接下来我们尝试仿照 LocalContentAlpha 制作一个 MyLocalContentAlpha。

首先定义 MyAlpha 数据类:

data class MyAlpha(val alpha: Float)

数据类中只包含一个 alpha 参数,表示透明度.

然后创建 MyContentAlpha 类:

object MyContentAlpha {
    val high = MyAlpha(1.0f)
    val medium = MyAlpha(0.74f)
}

这个类中定义了两个参数,high 表示完全不透明,medium 表示中等透明。

接下来定义 LocalMyContentAlpha 类:

val LocalMyContentAlpha = compositionLocalOf { MyContentAlpha.high }

通过 compositionLocalOf 构建出 CompositionLocal 对象。

创建 MyText 控件:

@Composable
fun MyText(text:String) {
    Text(text, modifier = Modifier.alpha(LocalMyContentAlpha.current.alpha))
}

MyText 控件中,使用 LocalMyContentAlpha.current.alpha 值作为 Text 的 alpha 值。

使用 CompositionLocal 隐式传参:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        MyText("Hello1")
        CompositionLocalProvider(LocalMyContentAlpha.provides(MyContentAlpha.medium)) {
            MyText("Hello2")
        }
        MyText("Hello3")
    }
}

在这个例子中,同样是将中间的控件放在 CompositionLocalProvider 作用域中,以对其中的控件应用中等透明度。

运行程序,效果和刚才一模一样。

小结

Composition Local 用于隐式传参,它类似于全局变量,但只会在其作用域内生效。好处是让其中的参数不需要层层传递,但作用域内的每层都能访问到。