Compose 不是把 XML 换成 Kotlin,而是把 UI 从“命令式更新 View”换成“状态驱动界面”。理解 State 和重组之后,Compose 的很多 API 才会变得顺。
核心结论
- Composable 函数描述某个状态下 UI 应该长什么样。
- State 变化会触发读取它的 Composable 重组。
remember负责在重组之间保存对象。- 状态应该尽量上提,让无状态 Composable 更容易复用和测试。
- 重组不是重建整个屏幕,Compose 会尽量跳过输入未变化的部分。
声明式 UI
传统 View 更像这样:
textView.text = user.name
button.isEnabled = user.isValid
Compose 更像这样:
@Composable
fun UserCard(user: User) {
Text(text = user.name)
Button(enabled = user.isValid, onClick = {}) {
Text("Submit")
}
}
你不再手动寻找某个 View 并修改它,而是声明:在当前状态下,UI 应该是什么样。
组合与重组
组合是第一次执行 Composable,生成 UI 树。
重组是状态变化后,Compose 再次执行受影响的 Composable,让 UI 和新状态保持一致。
关键点:
- 重组可能频繁发生;
- Composable 应该保持轻量;
- 不要在 Composable 函数体里直接执行不可控副作用;
- 可以被重复调用的代码才适合放在 Composable 里。
State 和 remember
State 的作用是通知 Compose:值变了,需要更新 UI。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("count = $count")
}
}
这里有两个角色:
remember:让count在多次重组之间保留下来;mutableStateOf:让count变化时触发重组。
如果没有 remember,每次重组都会重新创建状态。
状态提升
当一个 Composable 同时负责保存状态和展示 UI 时,它会比较难复用。
更推荐拆成:
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
CounterContent(
count = count,
onIncrease = { count++ }
)
}
@Composable
fun CounterContent(
count: Int,
onIncrease: () -> Unit,
) {
Button(onClick = onIncrease) {
Text("count = $count")
}
}
CounterScreen 管状态,CounterContent 只负责展示和发事件。
重组触发
重组常见来源:
- Composable 读取的
State发生变化; - 父 Composable 重组,并传入了新参数;
Flow/LiveData被转换成 Compose State 后发出新值。
典型链路:
ViewModel StateFlow 发出新值
-> collectAsStateWithLifecycle()
-> Compose State 更新
-> 读取该 State 的 Composable 重组
-> 子 Composable 根据参数变化决定是否重组
智能跳过
Compose 会尝试跳过输入没有变化的 Composable。
更容易被跳过的参数:
- 基本类型;
String;- 稳定的函数引用;
@Stable/@Immutable类型;- Compose 内置稳定类型。
需要避免:
- 每次重组都创建新的复杂对象;
- 在参数里传不稳定集合;
- 把过大的状态对象直接传到很深层级。
回看清单
- Compose 用状态描述 UI,而不是手动更新 View。
remember保存对象,State触发重组。- 状态尽量上提,UI 组件尽量无状态。
- 副作用用
LaunchedEffect、DisposableEffect等 API 管理。 - 性能问题先判断是重组、布局还是绘制阶段。