「这是我参加2022初次更文挑战的第4天,活动详情检查:2022初次更文挑战」。


数据的转化

关于 Compose 中的数据,用 mutableStateOf() 包裹来完成监听改写咱们已经驾轻就熟。那如果一个数据依赖于另一个或多个数据怎么办?这就用到 derivedStateOf(),它专门用于数据的转化,当上游数据发生改变时会主动改写下游数据,进一步改写 UI。有点类似 LiveData 中 Transformations.switchMap 的效果。

Show me the code:

setContent {
    var name by remember { mutableStateOf("chenhe") }
    val upperName by remember { derivedStateOf { name.uppercase() } }
    Column {
        Text(upperName)
        Button(onClick = { name = "bob" }) {
            Text(text = "Change Name")
        }
    }
}

upperNamename 转化得来,一开端会显现 “CHENHE”,点击按钮后变成 “BOB”。好了,本节到此结束


当然没那么简略 – –

为什么一定要经过包装才行呢?换句话说,套一层 derivedStateOf() 是为了解决什么问题?答案是:

  1. 上游数据改动时要重新处理。
  2. 上游数据没有改动时,不要重新处理。

诶?变则改写,不变就不改写… 听起来很像 remember 的用法?!那就试试看:

var name by remember { mutableStateOf("chenhe") }
val upperName = remember(name) { name.uppercase() } // <--------关键

name 作为 remember 的参数起到缓存 key 的效果,忘记的同学看看之前的笔记。这种写法同样能够完成需求。那… 还要 derivedStateOf() 干啥?

两者的本质区别

固然 derivedStateOfremember 都能够完成改写,但他们的原理不同,更准确来讲,是驱动不同。

关于前者的状况,当 name 改动,upperName 运用到了 name 因此发生改写。但由于无参 remember 的存在,upperName 的引证没有改动,依然是最开端的那个 MutableState,只不过内部的值变了,由于 derivedStateOf 完成了对上游数据的监听,它会重新调用转化函数,生成新的数据。

关于后者则有所不同。同样发生了改写,关于第二个 remember 来说 key 发生了改变,因此要重新履行初始化代码。初始化代码仅仅简略的一次性转化,没有监听功用。

总结,驱动 derivedStateOf 改写的是其内部的监听机制;而驱动 remember 改写的是它外部的 Recompose Scope。前者是主动,后者是被动。

So What?

derivedStateOf 的优势

在最简略的比如中,两个写法没有什么区别。那么咱们把单一的值换成 List 试试看:

setContent {
    val names = remember { mutableStateListOf("chenhe", "olivia") }
    val upperNames = remember(names) { names.map { it.uppercase() } }
    Column {
        Text(upperNames.joinToString())
        Button(onClick = { names.add("bob") }) {
            Text(text = "Change Name")
        }
    }
}

幻想中,点击按钮后应该显现三个名字。实际上,显现没有发生改变

此刻聪明的网友就要抢答了。这个我知道!names 的引证没有发生改变,仅仅内容变了,体系断定 key 没有发生改变。如果您想表达的是 Reference Equality 与 Structural Equality 的问题,那么又错了 ❌,请课后及时复习,key 的比较刚好便是结构性比较。

实在原因比这位网友的猜想还要简略:它在自己和自己比照! 好了,先停下来好好揣摩一下这句话再持续。

names 指向的目标没有改变,尽管做的是结构性比较,那也是当时结构与当时结构的比较,必定持平呀!得把当时结构和曩昔结构比较才能发现改变,不过咱们只有这一个 List 目标,上哪保存从前的结构去?还不明白的看这个比如:

val l1 = mutableListOf(1, 2)
val l2 = l1
l1.add(3)
l1 == l2  // Structural Compare: true

还不明白的,建议睡一觉 再接着学吧。

明显,remember + 参数的方式失效了。别告诉我直接删掉 remember 就行了,你不要功能了么。一个高雅的解决方案便是 derivedStateOf

val names = remember { mutableStateListOf("chenhe", "olivia") }
val upperNames by remember { derivedStateOf { names.map { it.uppercase() } } }

现在 upperNames 的改写由 derivedStateOf 内部驱动,不受 remember 控制了,但它不剩余:能够确保因其他原因导致重组时 upperNames 不会发生剩余的核算。

remember 的独到之处

学到这儿,又有好事者提出:既然如此,抛开功能不谈,是不是 remember 没啥用了,能够全部用 derivedStateOf 完成?那再看看这个比如:

setContent {
    var name by remember { mutableStateOf("chenhe") }
    UpperName(name) { name = "bob" }
}
@Composable
private fun UpperName(name: String, click: () -> Unit) {
    val upperName by remember { derivedStateOf { name.uppercase() } }
    Column {
        Text(upperName)
        Button(onClick = click) {
            Text(text = "Change Name")
        }
    }
}

咱们把一部分逻辑提取到了新的函数里。不卖关子了,点击后 UI 不会改变的。首先,upperNameremember 包裹所以不会二次初始化,这一点应该没什么问题。按照上文的剖析,derivedStateOf 内部会监听到改变而更新。问题就出在这儿。从前 derivedStateOf 代码块所运用到的是 MutableState,现在运用到的是一个普普通通的 String。String 没有「状况」,数值改动当然也就无法触发任何监听。

形象地总结:当一个被署理的变量作为参数传递的时候,就变成了普通的值,不再是 MutableState,导致监听链条断开,进而导致函数内部无法监听改变。

剖析了原因,解决方案也就水到渠成。有两个可行的做法:

  1. 强行传递 State,而不是普通的值。但这不是一个高雅的方案——它强制 caller 传递状况,而有时咱们仅仅想简略地显现一个字符串而已。

    setContent {
        val name = remember { mutableStateOf("chenhe") }
        UpperName(name) { name.value = "bob" }
    }
    @Composable
    private fun UpperName(name: State<String>, click: () -> Unit) {
        // name 是正宗的 State,能够被监听
        val upperName by remember { derivedStateOf { name.value.uppercase() } }
        Column {
            Text(upperName)
            Button(onClick = click) {
                Text(text = "Change Name")
            }
        }
    }
    
  2. 运用带参 remember

    @Composable
    private fun UpperName(name: String, click: () -> Unit) {
        val upperName = remember(name) { name.uppercase() }
        // ...
    }
    

总结

最佳实践

  1. 关于状况数据的转化,在同一个函数中,优先运用 derivedStateOf() + remember()
  2. 关于参数传递过来的无状况数据(String, Int 等),只能运用带参数的 remember()
  3. 关于参数传递过来的有内部状况的数据(SnapshotStateList 等),要运用 derivedStateOf() + 带参 remember()。带参数是避免传过来的目标自身引证发生改变,这种状况 derivedStateOf() 是监听不到的。

效果的区别

带参 remember():能够断定目标的重新赋值(引证改动),适合对函数参数运用。
derivedStateOf():适用于监听内部有状况的目标(可变目标)。