文章

Jetpack Compose 笔记(2) - remember

[上一节]({{< ref "JetpackCompose笔记(1).md" >}})总结了 Compose 中变量的三种写法:

val name = mutableStateOf("Bob")               // 1
val name by mutableStateOf("Bob")              // 2
val name by remember { mutableStateOf("Bob") } // 3

并且解释了前两个的区别。这一节来研究下最后一种。

开始之前,先看一个例子:

setContent {
    Column {
        var name by mutableStateOf("Bob")
        Text(text = name)
        Button(onClick = { name = "2" }) {
            Text(text = "Change Name")
        }
    }
}

看起来很简单,一开始会显示 "Bob",在点击按钮之后变成了 "2"。可是... 它依然显示 "Bob" 🤔

一层包装

我们已经知道,随着数据值的改变,可能会触发重组,那么 Compose 代码就要被重新执行。诶?代码怎么重新执行?Kotlin 可不是脚本语言。因此,Compose 将我们的代码包装进了一个 lambda 表达式。就好像 OnClickListener 那样,需要的时候可以再次执行里面的代码。这个包装叫做 Recompose Scope(重组作用域)。

包装有两个原则:

  1. 只包装有必要的。也就是那些可能发生变化的部分,例如我们的 Text
  2. 包装尽量精细。这样就可以局部刷新提高性能。

那么上面的代码经过包装,形式上有可能是这样的:

setContent {
  Column {
      WrapperFunction {
        var name by mutableStateOf("Bob")
        Text(text = name)
      }
      Button(onClick = { name = "2" }) {
        Text(text = "Change Name")
      }
  }
}

当再次执行 WrapperFunction 的时候,name 重新初始化,然后显示的文本是新的 name 对象所代表的值。—— 这一切发送在赋值之后。我们的赋值操作确实执行了,可惜刚刚赋值结束,原先的 name 就成了垃圾等待回收,被新 name 取代,而新的 name 值是重新初始化的 Bob

所以并不是赋值失败,也不是 UI 没刷新。而是刷新后的 name 已经不是曾经的那个它了。

知道原因,解决起来也就简单了:只需把变量定义放在作用域外面,例如 setContent 外面,就...行了???

确实行了,但某些时候做不到。例如我们写了一个 Compose Function 作为一个界面模块,给外部使用。那变量肯定要定义在内部呀,怎么办呢?

remember

终于回到主题。remember 有点类似 by lazy,只有在首次初始化时会执行里面的代码,否则直接返回已有的值。这样,即使处于重组作用域内,也不会多次初始化了。修改后的代码如下:

setContent {
    Column {
        var name by remember { mutableStateOf("Bob") }
        Text(text = name)
        Button(onClick = { name = "2" }) {
            Text(text = "Change Name")
        }
    }
}

那么什么时候应该加上 remember 呢?答:全都用就行了! 因为我们无法预知哪段代码可能会 recompose,所以最好的办法就是统统加上。但是在 composeable 函数外面是不用包的,显然,非 composeable 函数不可能被 recompose。其实,remember() 本来就是 composeable 函数,也无法在外面调用。

带参 remember

带参数的 remember 更像是一个缓存组件。它不仅可以防止重复执行,还可以设置 key,当 key 不同的时候就会刷新。同时 key 是 vararg 参数,可以根据实际需求设置多个 key。一个典型的用例是从网络上读取内容并显示:

@Composable
fun ShowMsg(url:String) {
    val msg = remember(url) {
        httpGet(url)
    }
    Text(msg)
}

这样以来,除非 url 发生变化,否则就不会重新读取内容了。

另外需要说明的是,key 的比较采用的是 Structural Equality 结构性相等,即 Kotlin 的 == 或 java 中的 equals()但是依然不适合用于对象内部发生变化的情况,原因与解决方案[后面揭晓]({{< ref "JetpackCompose笔记(4).md#derivedstateof-的优势" >}})。