上一篇聊到 Compose 性能调优,很多同学反馈:
“重组到底怎么触发的?为什么 remember 有时不生效?”
这其实不是性能问题,而是状态管理问题。Compose 的 UI 更新完全依赖状态,状态写不对,性能调优就是无本之木。
一、状态驱动 UI 的三个层次
@Composable
fun Counter() {
var count by remember { mutableIntStateOf(0) } // ① 状态
Button(onClick = { count++ }) { // ② 事件
Text("Count: $count") // ③ UI
}
}
Compose 的核心公式:State → UI。事件修改状态,状态变化触发重组。
三个层次要分清:
State:
mutableStateOf包装的可观察数据StateHolder:
ViewModel或remember层,负责状态的存活周期UI:根据状态描述当前界面
二、remember 不是缓存,是“按调用位置记忆”
很多人误以为 remember 是按 key 缓存,其实它按 Composable 在调用树中的位置记忆。
@Composable
fun Demo(flag: Boolean) {
if (flag) {
val name by remember { mutableStateOf("A") }
Text(name)
} else {
val name by remember { mutableStateOf("B") }
Text(name)
}
}
切换 flag 时,两个 remember 位于不同调用位置,状态不会共享。如果 flag 变化导致分支切换,旧状态会丢失。
正确做法是用稳定的 key:
val name by remember(flag) { mutableStateOf(if (flag) "A" else "B") }
三、副作用 API 的使用边界
Compose 有 5 个常用的副作用 API,用错很容易出 Bug:
典型错误:
LaunchedEffect(Unit) {
while (true) {
count++
delay(1000)
}
}
如果 count 是 Compose 状态,修改后触发重组,重组后 LaunchedEffect(Unit) 不会重启——这没问题。但如果把 count 作为 key,就会每秒取消重启一次,白白浪费。
四、重组的原理:只有读取了状态才会参与重组
@Composable
fun Parent() {
val state by viewModel.state.collectAsStateWithLifecycle()
ChildA(state.name)
ChildB(state.age)
}
当 name 变化时,只有 ChildA 会重组;ChildB 因为没读 name,会被智能跳过。
这就是 Compose 的细粒度重组。它依赖两个条件:
状态必须是
State类型,普通变量观察不到变化子 Composable 参数必须稳定(
@Stable或不可变数据类)
五、状态提升的正确姿势
官方建议“状态向下流动,事件向上传递”。简单说:
可复用的 UI 组件不要直接持有 ViewModel
状态尽量通过参数传入,事件通过 lambda 传出
@Composable
fun LoginForm(
phone: String,
onPhoneChange: (String) -> Unit,
onSubmit: () -> Unit
)
这样 LoginForm 既能在 ViewModel 场景用,也能在 Preview 里独立测试。
小结
Compose 的状态管理不难,关键是理解:
remember按调用位置记忆,不是按 key 全局缓存副作用 API 各有边界,别用
LaunchedEffect做普通回调只有读取了 State 的 Composable 才会参与重组
状态向下走,事件向上走,组件才容易复用
下一篇,我们离开声明式 UI,回到 View 体系,聊聊自定义 View 与 Canvas 绘制。