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了,希望这篇文章能帮到你,如果你喜欢这篇文章可以点个赞支持一下。


相关文章
|
3月前
|
程序员 测试技术
程序员难以一次性写好代码并持续修复Bug,主要源于软件的高复杂性、需求不确定性、测试局限性和技术能力限制。
【5月更文挑战第11天】程序员难以一次性写好代码并持续修复Bug,主要源于软件的高复杂性、需求不确定性、测试局限性和技术能力限制。复杂的系统易产生意外问题,需求变化导致初始设计难完备,测试无法覆盖所有情况,而技术更新和个体能力差异也会引入错误。因此,持续调试和优化是保证软件质量的关键步骤。
42 0
|
编译器 API
Compose:长期副作用 + 智能重组 = 若智?(一)
Compose:长期副作用 + 智能重组 = 若智?
122 0
|
3月前
|
前端开发 JavaScript 编译器
摆脱无用代码的负担:TreeShaking 的魔力
摆脱无用代码的负担:TreeShaking 的魔力
摆脱无用代码的负担:TreeShaking 的魔力
|
运维 IDE 数据可视化
摆脱重复操作,你值得拥有的自动化工具Automa
摆脱重复操作,你值得拥有的自动化工具Automa
摆脱重复操作,你值得拥有的自动化工具Automa
|
安全 编译器 开发者
Compose 的重组会影响性能吗?聊一聊 recomposition scope
很多人担心Compose的性能, 其实Compose编译器通过大量优化保证了recomposition的范围尽可能小,使得compose即使频繁重绘也不会有性能问题
599 0
|
人工智能 数据可视化 数据挖掘
后疫情时代,用数据支持业务恢复创造新的可能性
2020年可以说每一天都在见证历史,新冠疫情的突然造访就如同“黑天鹅”不期而至,而企业现在还不开始数字化转型就如同“灰犀牛”存在潜在风险,当下在黑天鹅和灰犀牛的夹击下,经济和市场都产生了巨大的影响。
|
Kubernetes 安全 Devops
功能无法停止交付,遗留的技术债务问题怎么解决
如果你曾在一家高速增长的软件工程公司待过,你可能会听过类似这样的一段对话,是关于技术债务的:
|
关系型数据库 Windows MySQL
在实际项目中遇到的一些有价值的问题,长期更新!
项目问题:生产库表名全是小写,而项目中部分表名使用的大写,          解决方法:需要开启mysql的忽略大小写配置,如图1-1所示:                                    ...
1028 0