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月前
|
缓存 前端开发 JavaScript
【揭秘Rails高手都在用的秘密武器!】—— 资产管道:它是如何悄无声息地改变我们管理前端资源的方式?
【8月更文挑战第31天】资产管道是Ruby on Rails 3.1引入的特性,用于简化Web应用中CSS、JavaScript和图片等前端资源的管理和打包。它将静态资源集中管理并自动处理合并、压缩及版本控制,提升页面加载速度和用户体验。本文通过示例代码详细介绍了如何在Rails应用中配置和使用资产管道,包括创建目录结构、编写样式表和JavaScript文件以及在布局文件中引用静态资源。与传统方法相比,资产管道提供了更高效和自动化的解决方案,有助于提高开发效率和应用性能。
28 0
|
3月前
|
测试技术 编译器 持续交付
持续部署的内涵和实施路径问题之集成尽早进行每次集成很小的问题如何解决
持续部署的内涵和实施路径问题之集成尽早进行每次集成很小的问题如何解决
|
3月前
|
物联网 测试技术 持续交付
持续部署的内涵和实施路径问题之持续部署过程中需要控制过程成本并保持高效的问题如何解决
持续部署的内涵和实施路径问题之持续部署过程中需要控制过程成本并保持高效的问题如何解决
|
编译器 API
Compose:长期副作用 + 智能重组 = 若智?(一)
Compose:长期副作用 + 智能重组 = 若智?
145 0
|
6月前
|
前端开发 JavaScript 编译器
摆脱无用代码的负担:TreeShaking 的魔力
摆脱无用代码的负担:TreeShaking 的魔力
摆脱无用代码的负担:TreeShaking 的魔力
|
存储 安全 数据管理
OushuDB 小课堂丨孤立数据迫在眉睫的威胁:废弃文件如何毁掉您的业务
OushuDB 小课堂丨孤立数据迫在眉睫的威胁:废弃文件如何毁掉您的业务
79 0
|
安全 编译器 开发者
Compose 的重组会影响性能吗?聊一聊 recomposition scope
很多人担心Compose的性能, 其实Compose编译器通过大量优化保证了recomposition的范围尽可能小,使得compose即使频繁重绘也不会有性能问题
621 0
|
人工智能 数据可视化 数据挖掘
后疫情时代,用数据支持业务恢复创造新的可能性
2020年可以说每一天都在见证历史,新冠疫情的突然造访就如同“黑天鹅”不期而至,而企业现在还不开始数字化转型就如同“灰犀牛”存在潜在风险,当下在黑天鹅和灰犀牛的夹击下,经济和市场都产生了巨大的影响。
|
Kubernetes 安全 Devops
功能无法停止交付,遗留的技术债务问题怎么解决
如果你曾在一家高速增长的软件工程公司待过,你可能会听过类似这样的一段对话,是关于技术债务的:
[转]国地税合并对企业的影响大吗
2018年6月15日上午,全国各省(自治区、直辖市)级以及计划单列市国税局、地税局合并且统一挂牌。国地税自1992年分手之后,再次合并管理,对于我们企业来说有哪些影响呢?