Jetpack Compose中传入Composable的回调为何会变更?
你对这个问题的猜想方向有部分是对的,但把机制想复杂了,核心逻辑非常好理解:
回调为什么会「自己变」
Compose的重组本质就是重新执行Composable函数的代码块,只要组件的上层发生重组,传给当前组件的回调lambda完全可能生成全新的实例,这个过程不需要你手动修改回调,甚至当前组件根本不需要离开Composition、一直显示在界面上都可能发生。
举个最常见的场景:
// 上层调用方代码 var currentUser by remember { mutableStateOf("游客") } // 当currentUser变化时,外层会触发重组 LandingScreen( onTimeout = { // 这个lambda捕获了外层的currentUser变量 navigateToHome(currentUser) } )
这段代码里你从来没有主动替换过传给LandingScreen的onTimeout,但只要currentUser的值变化触发外层重组,传给LandingScreen的就是一个全新的lambda实例——新lambda里捕获的是更新后的currentUser值,和上一次重组传入的lambda根本不是同一个对象,这就是文档里说的「回调发生了变化」。
为什么写死LaunchedEffect的key会出问题
部分副作用API支持传入可变数量的key参数,当任意key发生变化时会重启副作用。如果给LaunchedEffect传常量key比如true,这个协程只会在组件第一次加入视图树时启动一次,整个组件存活期间都不会重启。协程启动时会直接捕获当时传入的那个旧onTimeout实例,后续重组就算传了新的回调进来,正在运行的协程完全感知不到,等倒计时结束执行回调时,调用的还是最开始捕获的旧lambda,里面存的都是过时的外层状态,自然会出bug。
很多人第一反应是「那我把onTimeout当成key传给LaunchedEffect不就行了?」,但这样做只要回调变了协程就会重启,倒计时会从头开始算,完全不符合启动页只倒计时一次的需求。
rememberUpdatedState的实际作用
这个API没有什么黑魔法,它内部本质就是维护了一个可观察的状态值:每次当前组件发生重组,它都会自动把内部存储的引用更新成最新传入的回调。你在协程里访问currentOnTimeout时,拿到的永远是最近一次重组更新后的最新回调实例,既不需要重启协程重置倒计时,也不会调用到过时的旧回调。
标准的写法就是Codelab里给出的实现:
import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberUpdatedState @Composable fun LandingScreen(modifier: Modifier = Modifier, onTimeout: () -> Unit) { Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) { // 该引用始终指向LandingScreen重组时获取的最新onTimeout函数 val currentOnTimeout by rememberUpdatedState(onTimeout) // 创建与LandingScreen生命周期匹配的副作用 // 当LandingScreen重组或onTimeout变化时,倒计时不应重新开始 LaunchedEffect(true) { delay(SplashWaitTime) currentOnTimeout() } Image(painterResource(id = R.drawable.ic_crane_drawer), contentDescription = null) } }
对你原有猜想的纠正
- 回调变化不是组件离开Composition被回收导致的,组件正常显示过程中的普通重组就可能传入新的回调实例
- 不存在重组时传入空lambda的问题,Compose虽然不保证子组件的重组顺序,但只要组件在视图树中,传入的参数都是合法的函数实例,问题的核心只是长生命周期的协程捕获了旧的函数引用而已
你可以用一个生活化的例子类比记忆:
把
LaunchedEffect(true)启动的长任务想象成你约了个上门取件的快递员,你第一次下单时给快递员手写了一张写着家庭地址的纸条(旧回调),结果等快递员上门的半小时里你搬了家(外层状态变化触发重组,新回调传入),你没更新地址信息的话,快递员肯定会跑错到旧地址。rememberUpdatedState就相当于你给快递员留了个实时同步地址的取件码,他上门前扫一下就能拿到你最新的地址,永远不会跑错。
内容的提问来源于stack exchange,提问作者Andy Nguyen

