rememberUpdatedState()
副作用函数 DisposableEffect、LaunchedEffect 的 key 参数有两点作用:
- 能在其值改变的时候重启副作用函数,保证程序的正确性;
- 在值不变的情况下,就算其所在的 Composable 函数发生了重组,副作用代码也不会被重复执行,避免不必要的资源消耗。
那有没有一种情况:副作用函数内部所依赖的变量的值改变了,不需要重启副作用函数,也能在函数内部获取到这个变量的最新值?这样就能同时保证程序的正确性和性能了。
这当然是有的,比如:
副作用函数直接捕获 State 对象
@Composable
private fun RememberUpdatedStateDemo() {
val everyday =
listOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
var todayIndex by remember { mutableIntStateOf(value = 2) }
// todayString 是一个 State<String> 对象
val todayString by remember { derivedStateOf { everyday[todayIndex] } }
Column {
Text(text = "Today is $todayString")
Button(onClick = { todayIndex = (todayIndex + 1) % 7 }) {
Text(text = "Click to enter the next day")
}
}
// LaunchedEffect 的 key 为 Unit,不会重启
LaunchedEffect(Unit) {
delay(3000)
// 这里能获取到 todayString 的最新值
Log.d("Snow", "Today is indeed $todayString")
}
}
在程序运行后,如果我什么也不干,那么界面会显示 “Today is Wednesday”,并且 3s 后控制台也会打印 “Today is indeed Wednesday”。
如果我在 3s 内点击两次按钮,那么界面会显示 “Today is Friday”,并且 3s 后控制台也会打印 “Today is indeed Friday”。
为什么 LaunchedEffect 内部依赖的 todayString 的值改变了,LaunchedEffect 无需重启,也能拿到最新、改变后的值呢?
这是因为 todayString 是一个状态对象,并且由 remember 包着,使得它能跨越重组而长生,并且 todayString 对象的引用保持不变,LaunchedEffect 启动时捕获的是该对象的引用,所以它能读取到对象内部最新的值。
因此我们不需要这么写,才能保证程序的正确性:
LaunchedEffect(todayString) { // 将 todayString 作为 key
delay(3000)
Log.d("Snow", "Today is indeed $todayString")
}
当然你也可以这么写,但如果 todayString 变化频繁,这会导致 LaunchedEffect 频繁重启,会带来不必要的开销。
副作用函数依赖传递的普通类型参数
那我们再来看另外一种情况,我将 LaunchedEffect 相关的代码抽取到一个函数中:
@Composable
private fun RememberUpdatedStateDemo() {
val everyday =
listOf("Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday")
var todayIndex by remember { mutableIntStateOf(value = 2) }
val todayString by remember { derivedStateOf { everyday[todayIndex] } }
Column {
Text(text = "Today is $todayString")
Button(onClick = { todayIndex = (todayIndex + 1) % 7 }) {
Text(text = "Click to enter the next day")
}
}
// 传递的是 todayString 的值
MyLaunchedEffect(todayStringValue = todayString)
}
@Composable
private fun MyLaunchedEffect(todayStringValue: String) { // 参数是一个普通的 String 值
LaunchedEffect(Unit) {
delay(3000)
// 捕获的是 MyLaunchedEffect 首次被调用时传入的 todayStringValue 实例
Log.d("Snow", "Today is indeed $todayStringValue")
}
}
运行效果和之前唯一的不同是:在 3s 内点击两次按钮,界面会显示 “Today is Friday”,3s 后控制台打印的却是 “Today is indeed Wednesday”。
为何无法获取到最新值?
为什么会这样?
因为现在 LaunchedEffect 依赖的是函数参数 todayString,这是一个普通的字符串值。当 RememberUpdatedStateDemo() 函数因 todayIndex 的变化而重组时,会使用最新的 todayString 值去调用 MyLaunchedEffect() 函数。
而每一次调用都会创建新的String 实例,但已经启动的 LaunchedEffect 在其闭包中捕获的是第一次调用 MyLaunchedEffect 时传入的 String 实例(即 "Wednesday"),所以它不会自动感知到后续调用时传入的不同 String 实例,打印的仍然是 “Wednesday”。
一种解决方案是将变化的参数作为 LaunchedEffect 的 key,前面我们已经说过了,这不是一种理想的解决方案。
优雅的解决方案
我们可以在函数内部创建一个可跨越重组而存在的状态对象,并且每当 MyLaunchedEffect() 函数被重新执行(即参数 todayString 变化时),都要更新该状态对象的值为最新的参数值。
@Composable
private fun MyLaunchedEffect(todayStringValue: String) {
// 创建一个状态对象,保存 todayStringValue 的最新值
var currentStringState by remember { mutableStateOf(todayStringValue) }
// 更新值
currentStringState = todayStringValue
LaunchedEffect(Unit) {
delay(3000)
Log.d("Snow", "Today is indeed $currentStringState") // 读取状态对象的最新值
}
}
现在问题就解决了,在不重启 LaunchedEffect 的前提下,它能访问到最新的 todayString。
讲到现在,我想说的是这两行代码:
var currentStringState by remember { mutableStateOf(todayStringValue) }
currentStringState = todayStringValue
Compose 提供了一个函数,帮助我们简化,它就是 rememberUpdatedState()。
点进去它的定义处:
@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
mutableStateOf(newValue)
}.apply { value = newValue }
可以看到逻辑是一样的,也就是我们可以将上面的两行代码替换为:
val latestTodayStringValue by rememberUpdatedState(todayStringValue)
rememberUpdatedState() 详解
说到现在,我们就可以仔细说说rememberUpdatedState()函数了。
rememberUpdatedState 可以始终将最新的某个值(newValue)“包裹”在一个 State 状态变量中,它的值会被持续更新为最新的 newValue,并且该状态对象的引用在多次重组之间不变。
它主要用于那些生命周期比其所在 Composable 函数更长的场景中,特别是协程或副作用函数(如 LaunchedEffect, DisposableEffect, SideEffect 等) 。
对于 DisposableEffect,rememberUpdatedState 同样有用。例如,在 onDispose 的清理逻辑中,如果你需要引用某个在重组过程中可能发生变化的回调函数或状态,可以使用 rememberUpdatedState 可以确保 onDispose 执行的是基于最新值的逻辑。
场景:定时器回调与动态逻辑更新
假设一个实时监控系统有一个计时器,每秒调用一次回调。最开始回调内容是去监测服务器的状态,发现获取不到,再去检查网络连接状态。
@Composable
fun UseTimerDemo() {
var onTickCallback by remember {
mutableStateOf<() -> Unit>({ Log.d("Snow", "监测:当前服务器状态") })
}
// 模拟一个外部事件,在5秒后改变 onTickCallback 的逻辑
LaunchedEffect(Unit) {
delay(5000)
onTickCallback = { Log.d("Snow", "监测:当前网络连接状态") }
}
TimerExample(onTick = { onTickCallback() })
}
@Composable
fun TimerExample(onTick: () -> Unit) {
// 使用 rememberUpdatedState 来确保 latestOnTick 总是引用最新的 onTick lambda 实例
val latestOnTick by rememberUpdatedState(onTick)
LaunchedEffect(Unit) { // 这个 LaunchedEffect 只启动一次
while (true) {
delay(1000)
latestOnTick() // 始终调用最新的 onTick 实现
}
}
}
运行结果:
D/Snow: 监测:当前服务器状态
D/Snow: 监测:当前服务器状态
D/Snow: 监测:当前服务器状态
D/Snow: 监测:当前服务器状态
D/Snow: 监测:当前网络连接状态 // 5秒后,回调逻辑改变
D/Snow: 监测:当前网络连接状态
D/Snow: 监测:当前网络连接状态
...