开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情
我们稍微改动一下大话Compose筑基(1) - 掘金 (juejin.cn)介绍状态的实例代码为:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, true)
setContent {
Puzzle5()
}
}
@Composable
private fun Puzzle5() {
println("Puzzle5")
var name by mutableStateOf("Hello")
Text(name)
lifecycleScope.launch {
delay(3000)
name = "Hello World"
}
}
运行后发现并不是我们的预期效果。Text始终显示的是Hello。是没有触发重组吗?我们通过控制台日志可以看到每3秒打印了Puzzle5,那么说明是有重组发生的,但是注意这个时候name在重组的时候又会被mutableStateOf重新创建并给与初始值Hello,所以造成下面的Text(name)还是显示的Hello,那要怎么改呢?有两种解决思路分别是有状态和无状态
有状态or无状态
- 有状态 -> remember
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, true)
setContent {
Puzzle5()
}
}
@Composable
private fun Puzzle5() {
println("Puzzle5")
var name by remember { mutableStateOf("Hello") }
Text(name)
lifecycleScope.launch {
delay(3000)
name = "Hello World"
}
}
这里保持让Puzzle5可组合函数拥有name状态,也就是有状态。注意在创建状态的时候我们用了rememberapi去保存了这个状态。
可组合函数可以使用 remember API 将对象存储在内存中。系统会在初始组合期间将由 remember 计算的值存储在组合中,并在重组期间返回存储的值。remember 既可用于存储可变对象,又可用于存储不可变对象。
在可组合项中声明 MutableState 对象的方法有三种:
val mutableState = remember { mutableStateOf(default) }var value by remember { mutableStateOf(default) }val (value, setValue) = remember { mutableStateOf(default) }
这些声明是等效的,以语法糖的形式针对状态的不同用法提供。您选择的声明应该能够在您编写的可组合项中生成可读性最高的代码。
虽然 remember 可帮助您在重组后保持状态,但不会帮助您在配置更改后保持状态。为此,您必须使用 rememberSaveable。rememberSaveable 会自动保存可保存在 Bundle 中的任何值。对于其他值,您可以将其传入自定义 Saver 对象。
- 无状态 -> 状态提升
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, true)
//把状态交给了OnCreate方法
var name by mutableStateOf("Hello")
setContent {
//也可以把状态提升到这个地方。如果这样的话这个地方就必须用remember了,不然setContent重组的时候会覆盖
//var name by remember { mutableStateOf("Hello") }
//name状态作为参数给到Puzzle5函数使用,lamda交给调用函数去实现
Puzzle5(name) {
lifecycleScope.launch {
delay(3000)
//改变了状态后Puzzle5发生重组
name = "Hello World"
}
}
}
}
//不再有状态,状态提升了
@Composable
private fun Puzzle5(name: String, onValueChange: () -> Unit) {
println("Puzzle5")
Text(name)
onValueChange.invoke()
}
运行一下,嘿嘿!是我们的预期结果。三秒后页面变成了Hello World,日志在首次组合的时候打印一次,name改变后重组的时候打了一次。因为Puzzle不负责创建状态name,所以重组的时候不会覆盖之前的状态的值。这个时候我们的可组合函数Puzzle5就是无状态,他把状态提升给OnCreate函数了。
有状态和无状态我们应该怎么取舍呢?其实这个要根据使用的环境来定,在一开始我们其实不用过多的关心这个点,但需知一般在调用方不需要控制状态,并且不必自行管理状态便可使用状态的情况下,“有状态”会非常有用。但是,具有内部状态的可组合项往往不易重复使用,也更难测试。
无状态可组合项是指不保持任何状态的可组合项。实现无状态的一种简单方法是使用状态提升。
在开发可重复使用的可组合项时,您通常想要同时提供同一可组合项的有状态和无状态版本。有状态版本对于不关心状态的调用方来说很方便,而无状态版本对于需要控制或提升状态的调用方来说是必要的。
感兴趣的xdm可以看看官方文档专门讲状态提升的文档提升状态的场景 | Jetpack Compose | Android Developers,这个不属于筑基范畴故不会在本文深入。
重组
前面我已经了解了组合和状态。可组合函数组合起来就是组合,可组合函数的重组作用域订阅状态,状态改变会再次执行引用了该状态的可组合函数,这个过程就是重组了。我们用一个计数器的实现来当例子看看:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, true)
setContent {
println("SetContent")
var clickCount by remember { mutableStateOf(0)}
ClickCounter(clickCount) {
clickCount++
}
}
}
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
println("ClickCounter")
Button(onClick = onClick) {
println("Button")
Text("I've been clicked $clicks times")
}
}
这里有3处打印了日志,每次点击都会打印这三个日志,说明都发生了重组。那么肯定有人问,就改一个状态造成整个setContent的lamda表达式里面的所有函数重组,并且实际情况界面肯定比这复杂多了,并且里面还可能有成本高傲操作(在数据读取大量数据,大量绘制等),那么我这性能怎么办?那不掉帧?不ANR?带着这个疑问我们把上面的代码稍微改一下看看:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, true)
setContent {
println("SetContent")
var clickCount by remember { mutableStateOf(0)}
val scrollState = rememberScrollState()
Column (modifier = Modifier.fillMaxWidth().verticalScroll(scrollState)){
ClickCounter(clickCount) {
clickCount++
}
Puzzle2()
}
}
}
@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
println("ClickCounter")
Button(onClick = onClick) {
println("Button")
Text("I've been clicked $clicks times")
}
}
@Composable
fun Puzzle2() {
println("我重组了")
repeat (100) {
Text(text = "第${it + 1}个Text")
}
}
现在我们在界面加个组合函数Puzzle2来渲染一个列表,也加上一个日志打印。当我们点击按钮改变状态的时候打印出来日志并没有"我重组了"。说明在setContent的lamda表达式重组的时候,跳过了Puzzle2。那这是为什么呢?这个就必须说到重组的机制和重组作用域了。
重组作用域
Compose 的重组会影响性能吗?聊一聊 recomposition scope - 掘金 (juejin.cn)看这篇就够了。认真看后就了解重组作用域了,这篇文章就不再多说了。
小结
这篇我们通过例子介绍了组合函数的有状态和无状态。有状态要用到rememberapi,无状态会涉及到状态提升。然后看了两个重组的例子,看了一篇大神的文章认识了重组作用域。那么下篇我们就通过生命周期来进一步掌握重组的机制。
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情