rememberUpdatedState 本质上是为了让长生命周期的 effect / listener 闭包引用到最新的变量值,而不是重启 effect 或重新设置 listener。
原因是长生命周期的闭包捕获的是初始值
来看一个容易误解的例子
@Composable
fun Demo(name: String) {
LaunchedEffect(Unit) { delay(3000); println(name) }
}
假设初始传入的是 name = "A",1 秒后外部重组,name 变成了 "B"。很多人会直觉以为 3 秒后打印的是 "B",但实际上,大概率打印的是 "A"。
这是因为 LaunchedEffect 的 key 没有发生变化所以不会重启,而LaunchedEffect的协程启动时,闭包里捕获到的是当时启动那一刻的 name。也就是说,协程已经拿着旧值 "A" 跑出去了,后面的重组虽然让参数变成了 "B",但这个已经启动的协程不会自动换成新闭包。
解决方案有两种,主要看我们的业务逻辑,是否需要重启 effect。
- 第一种重启effect,将key Unit 变为 name 即可,但是每次 name 发生变化都会重新延迟3秒再打印name。
- 第二种rememberUpdatedState(name),不管name是否发生变化,只在3秒后打印一次name。
使用“引用类型“能解决闭包无法访问最新值的问题吗?
为什么“闭包”无法拿到最新值,这是作用域问题,就好比 Java 的自由变量必须声明为 final 就是防止出现这种问题,通常我们会将自由变量包装为“引用类型“,这样 Java 闭包就能修改自由变量引用的值了。所以我们可以试着这么做:
data class Ref(var value: String)
@Composable
fun Demo(name: String) {
var nameRef = remember { Ref(name) }
nameRef.value = name
LaunchedEffect(Unit) { delay(3000); println(nameRef.value) }
}
这种方式虽然解决了LaunchedEffect闭包无法访问最新值的问题,不过我们并不推荐这种写法,主要是以下两个问题:
- 不符合Compose语义,看起来莫名其妙,很多人估计都不知道闭包和自由变量的问题。
Ref不是 Compose state,所以修改ref.value不会触发重组。只是上面例子也没打算让它触发 UI 更新,所以从功能上来说没问题。
使用 rememberUpdatedState 提供稳定的容器
正确写法应该是使用Compose提供的rememberUpdatedState
@Composable
fun Demo(name: String) {
val currentName by rememberUpdatedState(name)
LaunchedEffect(Unit) { delay(3000); println(currentName) }
}
而它的源码思路和我们完全相同的,只是Compose用的是更符合语义State容器,并且能正确的触发依赖此State的子组件重组。
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
如何判断使用 rememberUpdatedState 的时机
最主要的依据是生命周期的长短,是否"跨越了多次重组"。
所以另一个典型的需要使用 rememberUpdatedState 的就是监听器,它的生命周期明显很长。例如:
@Composable
fun MyReceiver(onEvent: () -> Unit) {
val currentOnEvent by rememberUpdatedState(onEvent)
DisposableEffect(Unit) {
val listener = object : SomeListener {
override fun onSomething() {
currentOnEvent()
}
}
register(listener)
onDispose { unregister(listener) }
}
}
而明显不需要使用 rememberUpdatedState 有以下这些,他们是每次重组都执行了的:
- 普通 UI 读取参数
- 事件透传
- 根据值变化重启effect
// 普通 UI 读取参数
@Composable
fun Greeting(name: String) {
Text("Hello $name")
}
// 事件透传
@Composable
fun SaveButton(onSave: () -> Unit) {
Button(onClick = onSave) {
Text("Save")
}
}
// 根据值变化重启effect
@Composable
fun UserScreen(userId: String) {
LaunchedEffect(userId) {
loadUser(userId)
}
}