笔者曾经写过一篇关于新手入坑Jetpack Compose的文章,其中谈到了rememberUpdateState
的使用场景,但是最近的一次项目中还是踩坑了,而且收到了很多人反馈表示依然不理解如何正常使用这个Api,于是单独写一篇文章展开说说。
关于提到的文章传送门:妈!Jetpack Compose太难学了,别怕,这里帮你理清几个概念 - 掘金 (juejin.cn)
如果你完全不明白什么是智能重组、副作用,可以先看看笔者写的这篇文章。
1.长期副作用
在Jetpack Compose的世界中,所谓的长期副作用
基本就是等价于协程中的挂起函数,这个定义不一定对,但是足够覆盖绝大多数场景。
让我们看看一个长期副作用的样子:
@Composable fun LongRunningSideEffectExample(){ LaunchedEffect(Unit){ delay(1000) // TODO: 我是长期副作用 } }
可见,一个简单的长期副作用其实就是一个一段时间后才执行的逻辑,在大多数场景下,在delay结束后执行的逻辑都没有什么问题。
2.智能重组
众所周知,Jetpack Compose的编译器存在魔法,会在重组的时候,根据参数的是否发生了变化来决定是否充足当前的组件,这就是所谓的智能重组。
让我们看看一个智能重组的案例:
@Composable fun RecompositionExample( text:String ){ SideEffect { Log.d("重组记录","当前的值:$text") } Text( text=text ) }
SideEffect
Api会在重组成功后调用lambda,因此我们可以通过观察日志来查看当前组件的重组时刻,通过实验得知,只有text
参数发生变化的时候,SideEffect
的lambda才会被执行,这就是所谓的智能重组
,Compose会尽可能跳过没意义的重组。
3.长期副作用+智能重组=?
两者都是Jetpack Compose非常优秀的机制,但是两者在一起很容易出问题,例如下面这个组件:
@Composable @Preview fun LongRunningSideEffectWrongExample() { var count by remember { mutableStateOf(0) } Column { Button(onClick = { count++ }) { Text("当前的值:$count") } DelayOutputText(text = "$count") } } @Composable fun DelayOutputText( text: String, ) { var delayOutputText by remember { mutableStateOf("") } LaunchedEffect(Unit) { delay(3000L) delayOutputText = text } Text("延迟输出的值:$delayOutputText") }
组件非常简单,在出现DelayOutputText
3秒后,尝试显示最新的text值,但是实际运行结果如下:
可见,3秒后并没有显示最新的值,而是显示初始化的值,不是说智能重组吗,怎么没重组,问题出在哪里了?
让我们回到LaunchedEffect本身的源码:
@Composable @NonRestartableComposable @OptIn(InternalComposeApi::class) fun LaunchedEffect( key1: Any?, block: suspend CoroutineScope.() -> Unit ) { val applyContext = currentComposer.applyCoroutineContext remember(key1) { LaunchedEffectImpl(applyContext, block) } }
LaunchEffect
内部使用了一个remember
来包裹LaunchedEffectImpl
,总所周知,如果key
没有发生变化,remember
的lambda是不会重新被执行的,而我们通过LaunchedEffect
传入的block
参数,就在remember
的lambda中,这导致了一个问题:
如果LaunchedEffect的key没有发生变化,LaunchedEffect内部的lambda拿到的block参数是旧的
回到上文提到的出问题的代码,
笔者框住的这个代码块,看似是3秒后用最新的text
值赋值给delayOutputText
,实际上这是一种思维误区,真实的情况则是:如果key
没有发生变化的情况,即没有重启LaunchedEffect
的情况下,lambda一直都是最初的那个实例,那个lambda实例取的text
则是最初启动的时刻的值,因此3秒后,delayOutputText = text
这段代码,实际上是将text
第一次的值传给了delayoOutputText
,后续的text
值都被忽略了。
一切问题的根源是remember
是remember
忽视掉了新的lambda,最终执行的lambda都是最初那个,那么lambda内部的变量自然也是旧的了。
问题找到了,笔者想用一句经典的话来概括上述这段问题:
这不是一个bug,而是一个feature