Compose:长期副作用 + 智能重组 = 若智?(二)

简介: Compose:长期副作用 + 智能重组 = 若智?

4.让Compose再次智能


上述问题我们已经定位了,那么如何解决呢?这里提出两种解决方案:


4.1.让LaunchEffect重启


LaunchedEffect的本质是remember,因此在key发生变化的时候,LaunchedEffect会重启,我们把出问题的代码改成以下即可:


@Composable
fun DelayOutputText(
    text: String,
) {
    var delayOutputText by remember { mutableStateOf("") }
    //              👇🏻这里使用text作为key,发生变化的时候重启
    LaunchedEffect(text) {
        delay(3000L)
        delayOutputText = text
    }
    Text("延迟输出的值:$delayOutputText")
}

重新执行代码,发现没问题了,但是产生了另外一个问题:delay也重启了。这显然和我们的初衷是不一样的,因为我们希望的是3秒后显示最新的值,而不是值变化后又重启倒计时。

除非你的业务上就是要重启倒计时,否则通过修改key来获取最新值的方案是不符合需求的。

我知道你很急,你先别急,下面还有一种方案:


4.2.使用rememberUpdateState


先看看这个Api的源码:


@Composable
fun <T> rememberUpdatedState(newValue: T): State<T> = remember {
    mutableStateOf(newValue)
}.apply { value = newValue }

非常的简单,就是一个remember+mutableStateOf的常见组合再加上一个apply来完成赋新值。

既然如此简单,为什么官方还专门封装了一个这样的Api呢,因为上述提到的问题实在太普遍了,普遍到官方需要专门为这种场景封装一个语法糖。

看看如何使用这个Api来解决问题吧,把有问题的代码改造成如下:


@Composable
fun DelayOutputText(
    text: String,
) {
    //                                                   👇🏻包裹text
    val rememberText by rememberUpdatedState(newValue = text)
    var delayOutputText by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        delay(3000L)
        //                      👇🏻取值的时候使用包裹后的变量
        delayOutputText = rememberText
    }
    Text("延迟输出的值:$delayOutputText")
}

我们使用rememberUpdatedState来包裹住text,由于返回的是一个State,我们使用by委托来取值,重新运行后查看结果:

image.png

结果正确了,这是为什么呢,简单的Api居然解决了大问题,让我们简单分析下做了什么:

  1. 声明一个mutableState,使用text初始化它的值,text变化后,修改它的值
  2. 延时3秒后,从mutableState中取值

实际上我们就是用一个容器,即mutableState存住了text的值,延时结束后通过容器取值。remember没有重启,取的容器依然是最初那个,但是这并不影响,因为我们取的不是容器本身,而是容器内部的变量

去掉by委托会让答案更加清楚:


@Composable
fun DelayOutputText(
    text: String,
) {
    val rememberText: State<String> = rememberUpdatedState(newValue = text)
    var delayOutputText by remember { mutableStateOf("") }
    LaunchedEffect(Unit) {
        delay(3000L)
        //                      👇🏻容器还是旧的,但是容器的value变了,取的是最新值
        delayOutputText = rememberText.value
    }
    Text("延迟输出的值:$delayOutputText")
}

所以我们并没有去除remember没有重启的影响,而是通过一个容器来规避掉没有重启导致的取旧值的问题,我们不在乎取的是容器的旧值,因为这个容器内部的value是最新的即可。

这就是rememberUpdateState出现的原因,kotlin的lambda虽然方便阅读,但是太容易在Compose的重组场景下出现旧值问题,合理使用rememberUpdateState可以解决掉这个问题。


5.项目中还是踩了坑


笔者的项目代码大致如下:


@Composable
fun BoxContent(
    text: String,
) {
    TextContentWithLambda(
        onClick = {
            Log.d("临时测试", "当前的值:$text")
        }
    )
}
@Composable
private fun TextContentWithLambda(
    onClick: () -> Unit,
) {
    Row(
        Modifier,
        verticalAlignment = Alignment.CenterVertically
    ) {
        Box(
            Modifier
                .heightIn(30.dp)
                .background(Color.Black)
                .pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            onClick()
                        }
                    )
                },
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "点击",
                color = Color.White
            )
        }
    }
}

TextContentWithLambda做了一个类似手势监听的逻辑,然后点击后执行onClick(),但是BoxContent组件那个onClick取到的text依然是旧值。

思考了一大段时间后,笔者突然意识到,手势监听也有一个key作为重启标识,难道手势监听内部也是remember?打开源码一看:


fun Modifier.pointerInput(
    key1: Any?,
    block: suspend PointerInputScope.() -> Unit
): Modifier = composed(
    //省略
) {
    //省略
    remember(density) { SuspendingPointerInputFilter(viewConfiguration, density) }.also { filter ->
        LaunchedEffect(filter, key1) {
            filter.coroutineScope = this
            filter.block()
        }
    }
}

家人们谁懂啊,被remember坑到怀疑人生,问题找到了,还是同样的问题,由于remember导致了新的onClick并没有传递到内部,那么监听手势后执行的onClick自然也是旧的。

怎么解决这个问题呐,在kotlin中万物皆对象,高阶函数也是一个对象,那么我们可以使用rememberUpdateState把高阶函数包裹起来即可:


@Composable
private fun TextContentWithLambda(
    onClick: () -> Unit,
) {
    val rememberOnClick by rememberUpdatedState(newValue = onClick)
    //忽略
  }

最后把手势监听的onClick改成rememberOnClick即可。


总结


一切问题的根源就是remember机制导致新值被丢失,使用State作为容器让新值可以正常被访问,理解了这个原理就可以理解何时使用rememberUpdateState以及解决那些莫名其妙的bug了,希望这篇文章能帮到你,如果你喜欢这篇文章可以点个赞支持一下。


相关文章
|
7月前
|
编译器 API 容器
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
274 0
|
4月前
|
测试技术 持续交付 开发者
持续部署的内涵和实施路径问题之质量内建对持续部署有何重要性
持续部署的内涵和实施路径问题之质量内建对持续部署有何重要性
|
4月前
|
物联网 测试技术 持续交付
持续部署的内涵和实施路径问题之持续部署过程中需要控制过程成本并保持高效的问题如何解决
持续部署的内涵和实施路径问题之持续部署过程中需要控制过程成本并保持高效的问题如何解决
|
4月前
|
测试技术 编译器 持续交付
持续部署的内涵和实施路径问题之集成尽早进行每次集成很小的问题如何解决
持续部署的内涵和实施路径问题之集成尽早进行每次集成很小的问题如何解决
|
编译器 API
Compose:长期副作用 + 智能重组 = 若智?(一)
Compose:长期副作用 + 智能重组 = 若智?
156 0
|
数据库
重构——前提工作
重构——前提工作
|
存储 小程序 JavaScript
再也不用担心组件跨层级的数据共享和方法驱动了
再也不用担心组件跨层级的数据共享和方法驱动了
134 0
|
数据采集 机器学习/深度学习 存储
「数据战略」结果驱动的企业数据策略:数据生命周期过程
「数据战略」结果驱动的企业数据策略:数据生命周期过程
|
运维 IDE 数据可视化
摆脱重复操作,你值得拥有的自动化工具Automa
摆脱重复操作,你值得拥有的自动化工具Automa
摆脱重复操作,你值得拥有的自动化工具Automa
|
安全 编译器 开发者
Compose 的重组会影响性能吗?聊一聊 recomposition scope
很多人担心Compose的性能, 其实Compose编译器通过大量优化保证了recomposition的范围尽可能小,使得compose即使频繁重绘也不会有性能问题
633 0