Jetpack Compose -> 重组作用域和remember()

前言


上一章我们讲解了 MutableState 和 mutableStateOf() 本章我们讲解下 remember 这个关键方法;

ReCompose Scope(重组作用域)


我们先来看一段代码

当我们将

csharp 复制代码
var name by mutableStateOf("老A")

lifecycleScope.launch{}

这两行代码放到 setContent 中的时候,它提示红色了,显然这样写,Compose 是不允许的(这里其实是可以运行的,但是运行之后没有任何的效果,也就是 name 的值在3s后并没有变成 "Mars"),那么这是为什么呢?

这里面,我们写的协程是没有任何问题的,它也正常的执行了,但是真正影响我们效果的是 mutableStateOf 提示的那行代码;

我们前面有说到,Compose 会在使用到这些变量的值改变的时候得到通知,从而进行重组(recompose) ,例如 Text(name) 它会在 name 变化的时候得到通知,进行重组,但是我们来具体想一下,它怎么被执行的呢?比如说:我们现在需要写一段代码,需要在运行的时候,从界面上指定一段代码去重复执行,例如重复执行 Text(name) 这行代码,那么应该怎么实现呢?基本思路就是拿到这行代码,然后执行它,但是这好像不太能实现,在 java 也好 kotlin 中也罢,几乎是不太能实现的;

但是在 Compose 中,它替我们进行了实现,Compose 的编译器插件会修改逻辑,它会把这些可能会被重新调用的代码块给包了起来,然后在这个被包起来的代码块执行完成之后,它会把这个代码块保存下来,并标记到当前它被执行的这个位置,去做个标记,当重新执行的条件达成的时候,比如说:当 name 的值发生改变的时候,Text(name) 所在的代码块就会发生 Recompose 具体来说这个所在代码块它会重新执行,并且执行的时候它所依赖的变量值就是最新的值,从而就能基于最新值组合出最新的界面;如果组合的结果和上一次的结果不同,那么布局和绘制流程也会重新再走一遍,从而有更新界面的显示,如果组合的结果和上次的结果相同,说明界面的实质内容没有改变,则不会重新绘制;

Compose 并不是包裹所有的代码,而是包裹需要包裹的代码,什么是需要包裹的代码?就是可能会发生变化的代码,例如 Text(name) 这个 name 是一个变量,这个 name 改变了,这个 Text 就需要重新执行,那么这个就是需要被包裹的代码;

也就是说 ReCompose 并不是全局的,而是哪里需要做才做,这种被包裹起来在 ReCompose 的时候一起执行的代码范围就被 Compose 称作 ReCompose Scope(重组作用域)

所以代码执行的问题就在

csharp 复制代码
var name by mutableStateOf("老A")

这行代码上,它重新执行的时候,并不是 Text(name) 这一行了,而是一个范围了,这个范围内的代码都会被重新执行了,自然也就包含了 name 的初始化过程,那么这就意味着当 Text(name) 执行的时候,它所用到的值已经不是刚才被协程修改过的值了("Mars"),而是一个被重新初始化的值了,也就是 "老A" 这个字符串,本质上不是赋值失败,而是赋值之后触发了 ReCompose,并且 ReCompose 的范围波及到了更多的代码,导致重新创建了一个新的 name 对象,并且 Text(name) 重新调用的时候,采用的是这个新的值;而那个旧的 name 确实被改成了 "Mars" 但是 Text 用的不是这个旧的 name 了,而是一个新的 name;

那么怎么解决呢?把 Text(name) 包裹起来就可以了

scss 复制代码
var name by mutableStateOf("老A")
// 这样包裹一下
Button(onClick = { /*TODO*/ }) {
    Text(text = name)
}
lifecycleScope.launch {
    delay(3000)
    name = "Mars"
}

remember


但是,我们在业务开发的时候,肯定是不能这么写的(业务场景也不允许嘛),那么我们还有其他的方法来规避吗?我们来看下 IDE 有没有给提供解决方案,我们把鼠标放到警告的地方看下:

Creating a state object during composition without using remember

我们没有使用 remember 来创建一个 state object,也就是需要我们使用 remember 来包一下这个 state object

csharp 复制代码
var name by remember { mutableStateOf("老A") } 

可以看到红线警告消失了;

remember 所做的事情就是:在它第一次执行的时候,它会执行 lambda 中的代码 {mutableStateOf("老A")} 同时呢,它也会保存这个结果,再次调用这个 remember 的时候,它就会直接返回这个保存的结果,remember 在这里起到了一个缓存的作用;

我们把 Button 包裹去掉看下效果:

scss 复制代码
setContent {
    Android_VMTheme {
        Surface {
            var name by remember { mutableStateOf("老A") } 
            Text(text = name)
            lifecycleScope.launch {
                delay(3000)
                name = "Mars"
            }
            // Ui()
        }
    }
}

可以看到,3s 后变成了『Mars』,也就是那个缓存的值被改成了 Mars,二次初始化拿到的是老的值;

所以说这个 remember 在 Compose 中是很有用的,它可以防止由于 ReCompose 而导致的预期之外的某些变量的反复初始化,这些反复初始化的变量可能会带来意料之外的结果,但是加一个 remember 这些问题就可以解决了;

但是我们在实际业务开发的时候,怎么去判断哪些变量会被 ReCompose 呢?我们是无法判断的,那怕这段逻辑开发这很清楚,但是一旦被外部调用的时候,它就不可控了;

所以 Compose 针对所有的 mutableStateOf() 都加上 remember 包裹;

这里有一点要注意:我们用 remember 包裹是为了防止被 Compose 包裹的代码执行 ReCompose 从而导致变量的反复初始化带来的意料之外的结果;

如果变量的初始化没有被 Compose 包裹,也就是,我们把变量的初始化放到了外面

可以看到,remember 上进行了红色警告

@Composable invocations can only happen from the context of a @Composable function

也就是说我们不能使用 remember 关键字,所以说,无论 Text(name) 怎样 ReCompose 都不会导致 name 变量的重新初始化;

因为 remember 是一个 Composable 函数,它只能在另一个 Composable 函数中被调用

这个还会导致编译失败;

带参数的 remember

kotlin 复制代码
@Composable
fun Request(value: String) {
    val length = remember {
        value.length
    }
    Text(text = "长度为$length")
}

如果我们每次调用这个 Request 方法的时候,传递的 value 值都是一样的,那么这段代码执行起来就没有问题,一旦我们传入的值不一样的时候,那么这个段代码执行就有问题了;

例如:第一次 mars

第二次 mars

第三次 old a

这个时候,如果使用了 remember 那么 length 的长度就不对了,

怎么解决这个问题呢? remember 提供了带参数的方法,这个带参数的意思是:虽然我可以缓存,但是我要给缓存加上一个或者几个 key,如果 key 和上次一样,我就用缓存的,如果不一样,我就不使用缓存了;

kotlin 复制代码
@Composable
fun Request(value: String) {
    val length = remember(value) {
        value.length
    }
    Text(text = "长度为$length")
}

这个意思就是 value 会影响 lambda 中的计算结果,如果 value 没有变就不计算了,直接返回结果,变了就执行计算逻辑;

好了,今天的内容就到这里吧~~

下一章预告


无状态,状态提升,单向数据流

欢迎三连


来都来了,点个关注点个赞吧,你的支持是我最大的动力

相关推荐
找藉口是失败者的习惯2 小时前
Jetpack Compose 如何布局解析
android·xml·ui
Estar.Lee6 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh7 小时前
uiautomator案例
android
工业甲酰苯胺8 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3438 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee10 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯10 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey11 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!13 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟14 小时前
Android音频采集
android·音视频