「这是我参加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")
}
}
}
upperName
由 name
转化得来,一开端会显现 “CHENHE”,点击按钮后变成 “BOB”。好了,本节到此结束
当然没那么简略 – –
为什么一定要经过包装才行呢?换句话说,套一层 derivedStateOf()
是为了解决什么问题?答案是:
- 上游数据改动时要重新处理。
- 上游数据没有改动时,不要重新处理。
诶?变则改写,不变就不改写… 听起来很像 remember
的用法?!那就试试看:
var name by remember { mutableStateOf("chenhe") }
val upperName = remember(name) { name.uppercase() } // <--------关键
name
作为 remember
的参数起到缓存 key 的效果,忘记的同学看看之前的笔记。这种写法同样能够完成需求。那… 还要 derivedStateOf()
干啥?
两者的本质区别
固然 derivedStateOf
与 remember
都能够完成改写,但他们的原理不同,更准确来讲,是驱动不同。
关于前者的状况,当 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 不会改变的。首先,upperName
被 remember
包裹所以不会二次初始化,这一点应该没什么问题。按照上文的剖析,derivedStateOf
内部会监听到改变而更新。问题就出在这儿。从前 derivedStateOf
代码块所运用到的是 MutableState,现在运用到的是一个普普通通的 String。String 没有「状况」,数值改动当然也就无法触发任何监听。
形象地总结:当一个被署理的变量作为参数传递的时候,就变成了普通的值,不再是 MutableState,导致监听链条断开,进而导致函数内部无法监听改变。
剖析了原因,解决方案也就水到渠成。有两个可行的做法:
-
强行传递 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") } } }
-
运用带参
remember
✅@Composable private fun UpperName(name: String, click: () -> Unit) { val upperName = remember(name) { name.uppercase() } // ... }
总结
最佳实践
- 关于状况数据的转化,在同一个函数中,优先运用
derivedStateOf()
+remember()
。 - 关于参数传递过来的无状况数据(String, Int 等),只能运用带参数的
remember()
- 关于参数传递过来的有内部状况的数据(SnapshotStateList 等),要运用
derivedStateOf()
+ 带参remember()
。带参数是避免传过来的目标自身引证发生改变,这种状况derivedStateOf()
是监听不到的。
效果的区别
带参 remember()
:能够断定目标的重新赋值(引证改动),适合对函数参数运用。derivedStateOf()
:适用于监听内部有状况的目标(可变目标)。