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


目录
打赏
0
0
0
0
1
分享
相关文章
构建高效的Android应用:内存优化策略详解
随着智能手机在日常生活和工作中的普及,用户对移动应用的性能要求越来越高。特别是对于Android开发者来说,理解并实践内存优化是提升应用程序性能的关键步骤。本文将深入探讨针对Android平台的内存管理机制,并提供一系列实用的内存优化技巧,以帮助开发者减少内存消耗,避免常见的内存泄漏问题,并确保应用的流畅运行。
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
Compose:从重组谈谈页面性能优化思路,狠狠优化一笔
365 0
精简函数栈帧:优化创建和销毁过程的完全解析(建议收藏,提升内功)
精简函数栈帧:优化创建和销毁过程的完全解析(建议收藏,提升内功)
102 1
|
6月前
数据平台问题之在数据影响决策的过程中,如何实现“决策/行动”阶段
数据平台问题之在数据影响决策的过程中,如何实现“决策/行动”阶段
持续部署的内涵和实施路径问题之集成尽早进行每次集成很小的问题如何解决
持续部署的内涵和实施路径问题之集成尽早进行每次集成很小的问题如何解决
构建高效的Android应用:内存优化策略解析
【5月更文挑战第25天】在移动开发领域,性能优化一直是一个不断探讨和精进的课题。特别是对于资源受限的Android设备来说,合理的内存管理直接关系到应用的流畅度和用户体验。本文深入分析了Android内存管理的机制,并提出了几种实用的内存优化技巧。通过代码示例和实践案例,我们旨在帮助开发者识别和解决内存瓶颈,从而提升应用性能。
构建高效的Android应用:内存优化策略
【4月更文挑战第16天】 在移动开发领域,尤其是针对资源有限的Android设备,内存优化是提升应用性能和用户体验的关键因素。本文将深入探讨Android应用的内存管理机制,分析常见的内存泄漏问题,并提出一系列实用的内存优化技巧。通过这些策略的实施,开发者可以显著减少应用的内存占用,避免不必要的后台服务,以及提高垃圾回收效率,从而延长设备的电池寿命并确保应用的流畅运行。
Compose:长期副作用 + 智能重组 = 若智?(一)
Compose:长期副作用 + 智能重组 = 若智?
177 0

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等