Compose 状态机制和重组优化
- 列表自动刷新
- 重组优化
- 稳定性注解:@Stable
列表自动刷新
运行下面一段程序,点击按钮,会发生什么?
class StateObserverAndAutoUpdate : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val numbers by mutableStateOf(mutableListOf(1, 2, 3))
setContent {
NumberList(numbers = numbers)
}
}
@Composable
fun NumberList(numbers: MutableList<Int>) {
Column {
Button(onClick = {
numbers.add(numbers.last() + 1)
}) {
Text("点我:+1")
}
for (num in numbers) {
Text("number: $num")
}
}
}
}
运行效果图如下:
点击按钮会对numbers
的最后一个数进行+1,添加到集合尾部。内容确实更新了,表面上感觉会触发绑定了numbers
对象控件的刷新,实际上并没有。
这是因为add
方法,不会更改numbers
集合的对象,就意味不会触发numbers
的赋值`setValue
方法,也就不会触发mutableStateOf
状态变化,继而就不会触发Compose
的重组。
Compose
的重组才能让NumberList
方法被再次调用,完成Column
列表的刷新。
所以我们对numbers
集合的对象重新赋值时是不是就可以了:
class StateObserverAndAutoUpdate : ComponentActivity() {
var numbers1 by mutableStateOf(mutableListOf(1, 2, 3))
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NumberList(numbers = numbers1)
}
}
@Composable
fun NumberList(numbers: MutableList<Int>) {
Column {
Button(onClick = {
// 更新列表对象才能触发Compose的重组
numbers1 = numbers.toMutableList().apply {
add(numbers.last() + 1)
}
}) {
Text("点我:+1")
}
for (num in numbers) {
Text("number: $num")
}
}
}
}
但是,每次都要创建一个新的集合对象去替换之前的,这也太傻了吧。为啥不能监听集合内部的元素变化触发更新呢?
其实是有办法的,这里不行的根本原因是,我们用的是mutableStateOf
,它只会简单的订阅:是否赋值
如果我们用mutableStateListOf
就可以实现内部元素变化,触发刷新。
class StateObserverAndAutoUpdate : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val numbers = mutableStateListOf(1, 2, 3)
setContent {
NumberList(numbers = numbers)
}
}
@Composable
fun NumberList(numbers: MutableList<Int>) {
Column {
Button(onClick = {
numbers.add(numbers.last() + 1)
}) {
Text("点我:+1")
}
for (num in numbers) {
Text("number: $num")
}
}
}
}
同理,如果是Map集合,可以用mutableStateMapOf
:
val numberMap = mutableStateMapOf(1 to "1", 2 to "2")
重组优化
Compose
的重组优化,会将Compose
函数的一些不必要的更新操作跳过。解决自动更新带来更新范围过大,导致性能损耗。
setContent {
var text by remember {mutableStateOf("未点击")}
Column {
Button(onClick = {
text = "已点击"
}) {
Text(text = "点我更新内容")
}
Text(text = "内容:$text")
println("111")
RecomposeOptimization()
println("333")
}
}
@Composable
fun RecomposeOptimization() {
println("222")
}
按照正常的程序运行逻辑,点击“点我更新内容”的按钮后,会打印:
I/System.out: 111
I/System.out: 222
I/System.out: 333
但是,实际运行情况是:
I/System.out: 111
I/System.out: 333
发现少了222
的打印,这就是Compose
做的重组优化,RecomposeOptimization
函数和text
文本并无关联,所以并不需要因为text内容的变化做无谓的刷新,避免这部分的性能损耗。
这是简单的字符串变化,那如果是个对象,也能做到这种程度的优化吗?比如一个user
对象的变更
data class User(val name: String)
var clickText by mutableStateOf("点击前")
var user = User("张三")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Column {
println("000")
Button(onClick = {
println("click update name...")
clickText = "点击后"
// 点击后重新赋值一个同名的新对象
user = User("张三")
}) {
Text(clickText)
}
println("111:$clickText")
UpdateUser(user)
println("333")
}
}
}
@Composable
fun UpdateUser(user: User) {
println("222 user name is: ${user.name}")
Text(text = "name: " + user.name)
}
未点击按钮前的打印:
2022-05-01 15:54:58.915 3161-3161/com.example.playcompose I/System.out: 000
2022-05-01 15:54:58.927 3161-3161/com.example.playcompose I/System.out: 111:点击前
2022-05-01 15:54:58.928 3161-3161/com.example.playcompose I/System.out: 222 user name is: 张三
2022-05-01 15:54:58.929 3161-3161/com.example.playcompose I/System.out: 333
点击按钮后重新赋值一个同名的新对象,打印结果:
2022-05-01 16:03:05.904 3422-3422/com.example.playcompose I/System.out: 000
2022-05-01 16:03:05.925 3422-3422/com.example.playcompose I/System.out: 111:点击后
2022-05-01 16:03:05.926 3422-3422/com.example.playcompose I/System.out: 333
发现222
没有打印,证明虽然对象变化了,但是由于新的对象名字没有变化,Compose
重组也对引用对象的做了优化。由此可猜测背后是根据实例对象的equal
方法去做对象是否“真正变化”的辨别。
有趣的是,如果把User
类的val
改为var
:
data class User(var name: String)
点击后的打印结果又会变成:
2022-05-01 16:08:50.857 3791-3791/com.example.playcompose I/System.out: 000
2022-05-01 16:08:50.860 3791-3791/com.example.playcompose I/System.out: 111:点击后
2022-05-01 16:08:50.860 3791-3791/com.example.playcompose I/System.out: 222 user name is: 张三
2022-05-01 16:08:50.860 3791-3791/com.example.playcompose I/System.out: 333
发现重组优化的效果又消失了。这是为什么呢?
这是因为var
的可变性的特性,导致name
可能会在某个时刻被修改,这是不可预期的,如果Compose
此时因为去优化不去执行刷新操作,会出现不可预期的错误。所以在效率和正确性上,Compose
选择了正确性,如果你使用了var
,不论你对象或对象的值是否改变,就无条件刷新,不再提供重组优化。
稳定性注解:@Stable
但想想因为一个var
,就无脑跳过这种重组优化,是否也太粗暴了呢?
如果我们对上述的类加上@Stable
:
@Stable
data class User(var name: String)
会发现,它又可以进行重组优化,跳过了不需要的更新。
@Stable
它就相当于一个开关,如果开了就等于说开发者可以保证这个类的稳定,不会出现数据的意外被修改现象。
虽然是可以做到数据的稳定性,但实际上很难保证,所以不太推荐轻易使用该关键字。@Stable
注解常给接口使用。
对于普通类,推荐使用以下使用参数代理的常用写法:
class User(name: String) {
var name by mutableStateOf(name)
}
通过by mutableStateOf
就可以让Compose
不通过@Stable
关键字,就确定该类是可靠的,它会根据name
的变化,通知到绑定页面的自动刷新。