Jekpack Compose “状态订阅&自动刷新” 系列:
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - MutableState/mutableStateOf 】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - remember 和重组作用域 】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - 有状态、无状态、状态提升?】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - mutableStateListOf 】
【 聊聊 Jetpack Compose 的 “状态订阅&自动刷新” - - 你真的了解重组吗?】
在讲本篇文章主题之前,建议先看看 聊聊 Jetpack Compose 原理 – 状态订阅&自动刷新机制 一文,因为两篇文章是上下篇的关系,看完上篇,可以更好的串联知识点。
话不多说,还是老样子,从 Demo 一步步引出我们的核心知识点。
一、话题引入
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
var name by mutableStateOf("Compose")
setContent {
Text(name)
LaunchedEffect(true) {
delay(3000)
name = "Kotlin"
}
}
}
}
一段很简单的代码示例,就是前一篇文章的开头示例,我们看下效果:
很简单,3s 后从 Compose 变为 kotlin。
现在我们改下代码:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var name by mutableStateOf("Compose")
Text(name)
LaunchedEffect(true) {
delay(3000)
name = "Kotlin"
}
}
}
}
我们把 var name by mutableStateOf("Compose")
从 setContent 外面挪到里面来了。
执行看下效果:
奇怪了,文字没有刷新!
二、重组作用域
问题出在哪?首先我们回顾下 Compose 是怎么刷新界面的?比如上面的例子,当 name 重新赋值后,读取它的地方会被标记为失效,然后重组刷新。
在 Compose 中并不是单纯的刷新 Text(name) 这一行,而是会把包含 Text(name) 的代码块给包起来,刷新的是整个代码块,或者说重组整个代码块:
所以这段蓝色背景的代码块会被重新执行一遍,这个蓝色区域就是:重组作用域(Recompose scope)!
所以问题原因你应该就能找到了:
不仅仅 Text(name)
会被重新执行,var name by mutableStateOf("Compose")
也会执行!name 又被重新初始化了。
三、remember
那怎么解决?其实开发工具已经提示你了:
mutableStateOf
是标红的,并且错误提示:Creating a state object during composition without using remember.
意思就是:你在组合过程里面创建了 StateObject 对象,但是没有用 remember。
所以怎么包?如下:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
var name by remember { mutableStateOf("Compose") }
Text(name)
LaunchedEffect(true) {
delay(3000)
name = "Kotlin"
}
}
}
}
加了 remember 后,在第一次执行的时候,会执行 Lambda 表达式,也就是执行 mutableStateOf("Compose")
,并且 remember 会保存结果(StateObject),再次调用的时候会直接返回保存的老对象(StateObject),而不是在执行 Lambda 表达式里面的代码,相当于充当了缓存的功能。
我们执行看下:
所以 remember 起到缓存作用,就是为了防止多次初始化变量而导致程序不可控,所以在 Compose 里面你只要 mutableStateOf,那么能加 remember 我们就加 remember。
另外我们需要注意 remember 是可以带参数的:可以一个或者多个参数。
@Composable
inline fun <T> remember(
key1: Any?,
calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(currentComposer.changed(key1), calculation)
}
@Composable
inline fun <T> remember(
key1: Any?,
key2: Any?,
calculation: @DisallowComposableCalls () -> T
): T {
return currentComposer.cache(
currentComposer.changed(key1) or currentComposer.changed(key2),
calculation
)
}
有什么用,我们举个例子说明,比如我们自定义了一个 Composable 函数:
@Composable
fun showCharCount(value: String) {
val length = value.length
Text("字符串长度:$length")
}
很简单,一个显示字符串长度的函数。
我们假设一场场景:
- 传入进来的字符串特别长,
- 并且 showCharCount 反复被调用了很多次。
那么 value.length 每次都被调用,就显得有点笨重了,所以我们可以给它加上一个 remember。
@Composable
fun showCharCount(value: String) {
val length = remember {
value.length
}
Text("字符串长度:$length")
}
但此时就会出现一个问题:
@Composable
fun showCharCount(value: String) {
// 第一次传进来:abcd
// 第二次传进来:abcdefg
// 第三次传进来:abcdefghijklmn
val length = remember {
value.length
}
Text("字符串长度:$length")
}
字符串长度永远都是:4,因为 value.length
不会被执行,那怎么办:
@Composable
fun showCharCount(value: String) {
// 第一次传进来:abcd
// 第二次传进来:abcdefg
// 第三次传进来:abcdefghijklmn
val length = remember(value) {
value.length
}
Text("字符串长度:$length")
}
我们给 remember 加了一个参数:value,只要这个 key 变化了,那么就会重新执行 Lambda 表达式,这就是带参数的 remember 的用法。