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闭包无法访问最新值的问题,不过我们并不推荐这种写法,主要是以下两个问题:

  1. 不符合Compose语义,看起来莫名其妙,很多人估计都不知道闭包和自由变量的问题。
  2. 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)
    }
}