Jeptpack Compose 官网教程学习笔记(四)番外-CompositionLocal

CompositionLocal是通过组合隐式向下传递数据的工具

主要学习内容

  • 了解什么是 CompositionLocal
  • 创建自己的 CompositionLocal
  • 何时使用CompositionLocal

显示传参与隐式传参

显示传参

@Test
fun Surface() {
    val textColor = "红色"
    Column(textColor)
}

fun Column(textColor: String) {
    println("Column color:$textColor")
    Button(textColor)
}

fun Button(textColor: String) {
    println("Button color:$textColor")
    Text(textColor)
}

fun Text(textColor: String) {
    println("Text color:$textColor")
}

显示传参中数据以参数的形式向下流经整个调用过程,可以看到为了传递textColor参数需要每个调用方都增加textColor参数

隐式传参

var textColor = "红色"

@Test
fun Surface() {
    Column()
}

fun Column() {
    println("Column color:$textColor")
    Button()
}

fun Button() {
    println("Button color:$textColor")
    Text()
}

fun Text() {
    println("Text color:$textColor")
}

隐式传参中数据以共有数据的形式传递,不需要额外参数

注:此时的SurfaceColumn等只是普通函数

函数内修改值

显示传参

@Test
fun Surface() {
    val textColor = "红色"
    Column(textColor)
    println("----------")
    Column(textColor)
}

//未修改Column,省略Column代码

fun Button(textColor: String) {
    textColor="黑色"
    println("Button color:$textColor")
    Text(textColor)
}

//未修改Text,省略Text代码
Column color:红色
Button color:黑色
Text color:黑色
----------
Column color:红色
Button color:黑色
Text color:黑色

显示传参中,修改数据的值不会影响下一次调用,即数据隔离效果。而且可以影响下游的值

隐式传参

var textColor = "红色"

@Test
fun Surface() {
    Column()
    println("----------")
    Column()
}

//未修改Column,省略Column代码

fun Button() {
    textColor="黑色"
    println("Button using:$textColor")
    Text()
}

//未修改Text,省略Text代码
Column color:红色
Button color:黑色
Text color:黑色
----------
Column color:黑色
Button color:黑色
Text color:黑色

第一次调用没有问题,但第二次调用时textColor就全为黑色,即隐式传参中值的修改不是局部的,会导致其他地方对于textColor的使用发生变化

那么若我们只想在Button及其子元素中修改textColor值,在其他地方仍为原来的值该怎么办呢?

我们可以记录textColor原本的值,在使用完后再将原本的值重新赋值给textColor

fun Button() {
    val origin = textColor
    textColor = "黑色"
    println("Button color:$textColor")
    Text()
    textColor = origin
}

在kotlin中我们还可以进一步封装该过程

fun Button() {
    provider("黑色") {
        Text()
        println("Button color:$textColor")
    }
}

fun provider(changed: String, action: () -> Unit) {
    val origin = textColor
    textColor = changed
    action()
    textColor = origin
}

虽然这个代码比较简易,但是与CompositionLocal的原理大同小异。可以基于这个大致理解CompositionLocal,若想深入理解CompositionLocal需要去查阅源码

简介

在 Compose 中往往可组合项调用另一个可组合项,若所有数据都以参数形式向下流经整个界面树传递给每个可组合函数,那么对于广泛使用的常用数据(如颜色或类型样式),这可能会很麻烦,无论是在编写代码还是维护代码时

为了支持无需将颜色作为显式参数依赖项传递给大多数可组合项,Compose 提供了 CompositionLocal,可让您创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式

CompositionLocal 元素通常在界面树的某个节点以值的形式提供。该值可供其可组合项的后代使用,而无需在可组合函数中将 CompositionLocal 声明为参数

MaterialTheme对象中提供了三个 CompositionLocal 实例,即 colors、typography 和 shapes。我们可以在之后的可组合函数中检索这些实例

具体来说,这些是可以通过 MaterialTheme colorsshapestypography 属性访问的 LocalColorsLocalShapesLocalTypography 属性

@Composable
fun MyApp() {
    MaterialTheme {
        SomeTextLabel("CompositionLocal")
    }
}

@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // 通过LocalColors隐式传值
        color = MaterialTheme.colors.primary
    )
}

CompositionLocal 实例的作用域限定为组合的一部分,因此您可以在结构树的不同级别提供不同的值。CompositionLocalcurrent 值对应于该组合部分中最接近的祖先提供的值

CompositionLocal作用域分析.png

Column中修改了CompositionLocal的值,于是在组合树中分为了两个作用域范围

ListItem中获取CompositionLocalcurrent 值就对应于Column提供的值

BottomNavigation中获取CompositionLocalcurrent 值就对应于Scaffold提供的值,而不是Column提供的值,因为Column不是BottomNavigation的祖先节点而是兄弟节点

如需为 CompositionLocal 提供新值,请使用 CompositionLocalProvider 及其 provides infix 函数,该函数将 CompositionLocal 键与 value 相关联。在访问 CompositionLocalcurrent 属性时,CompositionLocalProvidercontent lambda 将获取提供的值。提供新值后,Compose 会重组读取 CompositionLocal 的组合部分

@Composable
fun CompositionLocalExample() {
    //MaterialTheme中LocalContentAlpha值默认为ContentAlpha.high
    MaterialTheme {
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                //通过使用其`current`属性获取`CompositionLocal`当前值
                Text("This Text also uses the medium value:${LocalContentAlpha.current}")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProvider可以跨函数作用
    Text("This Text uses the disabled alpha now")
}
预览效果

Material 可组合项会在内部使用CompositionLocal,我们可以通过使用其current属性获取CompositionLocal当前值

注意CompositionLocal 对象或常量通常带有 Local 前缀,以便在 IDE 中利用自动填充功能提高可检测性

自定义CompositionLocal

CompositionLocal通过组合隐式向下传递数据的工具

使用 CompositionLocal 的另一个关键点是该参数是横切参数(隐式传参)且中间层的实现不需要知道该参数的存在。例如,对 Android 权限的查询是由 CompositionLocal 在后台提供的。媒体选择器可组合项继续添加新功能,无需根据权限是否获取去修改其API。只需要媒体选择器的调用方知道权限的获取情况

这里吐槽一句,辣鸡谷歌机翻(╯' - ')╯︵ ┻━┻

但是,CompositionLocal 并非始终是最好的解决方案。不建议过度使用 CompositionLocal,其存在一些缺点:

  • CompositionLocal 使得可组合项的行为更难推断

    Material 组件中大量使用CompositionLocal方式传递值,若不熟悉这些组件则很难判断出组件最后呈现效果和应修改那些CompositionLocal

  • 在创建隐式依赖项时,使用这些依赖项的可组合项的调用方需要确保为每个 CompositionLocal 提供一个值

    该依赖项可能没有明确的可信来源,因为它可能会在组合中的任何部分发生改变,会使得测试时难度变高

CompositionLocal 非常适合基础架构,而且 Jetpack Compose 大量使用该工具

使用条件

CompositionLocal 应具有合适的默认值。如果没有默认值,在开发时很容易陷入不提供CompositionLocal 导致的异常

如果创建测试或预览使用该 CompositionLocal 的可组合项时也需要显式提供默认值,那么不提供默认值不仅会带来问题还会造成了糟糕的使用体验

有些概念并非以树或子层次结构为作用域,请避免对这些概念使用 CompositionLocal,建议使用CompositionLocal 的情况为:其可能会被任何(而非少数几个)后代使用

一种错误做法的示例是创建在特定界面使用的 ViewModelCompositionLocal,以便该屏幕中的所有可组合项都可以获取 ViewModel 来执行某些逻辑

因为特定界面下并不是所有可组合项都需要知道ViewModel。最佳做法是使用状态向下传递而事件向上传递的单向数据流模式,或只向可组合项传递所需信息。这样做会使可组合项的可重用性更高,并且更易于测试

创建CompositionLocal

有两个 API 可用于创建 CompositionLocal

  • compositionLocalOf:如果更改提供的值,会使读取其 current 值的组件发生重组
  • staticCompositionLocalOf:与 compositionLocalOf 不同,Compose 不会跟踪 staticCompositionLocalOf 的读取。更改该值会导致提供 CompositionLocal 的整个 content lambda 被重组,而不仅仅是在组合中读取 current 值的位置

如果为 CompositionLocal 提供的值发生更改的可能性微乎其微或永远不会更改,使用 staticCompositionLocalOf 可提高性能

例:

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// 定义一个带有默认值的compostionlocal全局对象
// 这个实例可以被应用中的所有可组合项访问
val LocalElevations = compositionLocalOf { Elevations() }

为CompositionLocal提供值

CompositionLocalProvider 可组合项可将值绑定到给定层次结构的 CompositionLocal 实例。如需为 CompositionLocal 提供新值,请使用 provides infix 函数,该函数将 CompositionLocal 键与 value 相关联,如下所示:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // 基于系统主题创建不同的Elevations
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }

            // 将elevations赋值给LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // 可组合项都可以通过LocalElevations.current获取到elevations实例
            }
        }
    }
}

使用CompositionLocalProvider

CompositionLocal.current 返回由最接近的 CompositionLocalProvider(其向该 CompositionLocal 提供一个值)提供的值:

@Composable
fun SomeComposable() {
    Card(elevation = LocalElevations.current.card) {
        // Content
    }
}

替换方案

在某些情况中,CompositionLocal 可能是一种过度的解决方案。如果您的用例不符合CompositionLocal使用条件,其他解决方案可能更适合您的用例

传递显示参数

显式使用可组合项的依赖项是一种很好的习惯。建议仅传递所需可组合项。为了鼓励分离和重用可组合项,每个可组合项包含的信息应该可能少

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

// ✘ 不要传递整个对象! 应该只传递需要的部分
// 同样也不要使用 CompositionLocal 隐式传递 ViewModel
@Composable
fun MyDescendant(myViewModel: MyViewModel) { ... }

// 只传递需要的部分
@Composable
fun MyDescendant(data: DataToDisplay) {
    // Display data
}

控制反转

即不是由后代接受依赖项来执行某些逻辑,而是由父级接受依赖项来执行某些逻辑,也就是状态向下传递而事件向上传递的单向数据流模式

在以下示例中,后代需要触发请求以加载某些数据:

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

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

此时我们可以考虑将MyDescendantButton的点击事件上传,在MyComposable中执行myViewModel.loadData(),即事件上传

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

此方法将子级与其直接祖先实体分离开来将子级与其直接祖先实体分离。虽然祖先实体可组合项往往越来越复杂,这样就可以使更低级别的可组合项更灵活

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容