状态是什么
Compose 是声明式 UI:界面是状态的函数,状态变则界面变。这里的「状态」指任何随时间变化、能驱动 UI 的值——输入框文字、列表数据、开关选中、加载状态都算。
声明式的核心承诺:只改数据,界面自动跟着改。
因此状态管理的本质问题只有两个:状态放哪、状态怎么变。放得近则简单直接,放得远则共享方便,怎么变则决定谁能触发重组。
remember 与状态持有
remember 让值在重组之间存活。普通变量每次重组都重建,remember 包住的值只建一次。remember 内部用 mutableStateOf 包装,Compose 才能感知变化。
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击 $count 次")
}
}
by 委托写法最简洁,count 读写都自动命中 State。注意 remember 只在组合内存活,配置变更(旋转屏幕)后也会重置;要跨配置存活,用 rememberSaveable。
mutableStateOf 与状态读写
mutableStateOf 创建可观察状态,读写它就会触发依赖它的组件重组。状态粒度要适中:整页一个大状态和每字段一个状态,性能天差地别。
@Composable
fun LoginForm() {
var username by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var loading by remember { mutableStateOf(false) }
Column {
OutlinedTextField(value = username, onValueChange = { username = it })
OutlinedTextField(value = password, onValueChange = { password = it })
Button(enabled = !loading, onClick = { loading = true }) {
Text(if (loading) "提交中" else "登录")
}
}
}
细粒度状态让每次输入只重组输入框本身,而不是整屏。能用基本类型就别装箱,能拆开就别捆在一起。
状态提升与重组治理
状态提升:把状态挪到共同父级,子组件通过参数收状态、通过回调改状态。无状态组件可复用、可测试,这是 Compose 组件设计的黄金法则。
@Composable
fun TodoItem(
text: String,
done: Boolean,
onToggle: () -> Unit
) {
Row {
Checkbox(checked = done, onCheckedChange = { onToggle() })
Text(text)
}
}
@Composable
fun TodoScreen() {
var items by remember { mutableStateOf(listOf(Todo("学习状态", false))) }
TodoItem(text = items[0].text, done = items[0].done) {
items = items.map { it.copy(done = !it.done) }
}
}
重组治理还有两招:lambda 参数抽成 remember 避免每次重建、稳定类型用 immutable 标注。列表用 LazyColumn,别在 Column 里循环塞上千行。
关于重组范围,很多初学者容易误解:Compose 的重组是智能分片的,状态变化只会让读取了该状态的代码块重新执行,而不是整个组件树全部重建。利用这个特性,可以把耗时的计算放到 remember 里,把不必要的读取从重组频繁的节点移出去。另一个常见问题是派生状态的处理,列表过滤、金额格式化这类计算结果应该用 derivedStateOf 缓存,避免每次重组都重新算一遍;纯 UI 展示数据则可以直接读取,不需要额外包装。调试重组时,可以用 Layout Inspector 的 Compose 面板看重组计数,如果某个高频交互导致整页大范围重组,通常说明状态提升的层级放低了,把状态上移、把无状态组件下沉,是治理重组问题最有效的一招。
StateFlow 与数据层
数据来自网络或数据库时,别在 UI 里裸接。ViewModel 持有 StateFlow,UI 用 collectAsStateWithLifecycle 收集,生命周期感知自动处理停止收集。
class TodoViewModel(private val repo: TodoRepo) : ViewModel() {
private val _uiState = MutableStateFlow(TodoUiState())
val uiState: StateFlow<TodoUiState> = _uiState.asStateFlow()
fun refresh() {
viewModelScope.launch {
_uiState.update { it.copy(loading = true) }
val data = repo.loadTodos()
_uiState.update { it.copy(items = data, loading = false) }
}
}
}
// 组合中
val state by viewModel.uiState.collectAsStateWithLifecycle()
UI 状态和业务数据分开建模:UiState 含 loading、error、items,一个状态对象驱动整屏,别让数据裸奔进组件。
收集 StateFlow 时还可以用 distinctUntilChanged 做去重,列表数据没变化就不触发重组,进一步减少无谓的界面刷新。
实战:待办列表状态管理
把前面所有技巧拼起来:rememberSaveable 存本地输入、StateFlow 管数据、状态提升做列表项、derivedStateOf 算未完成数。
val unfinishedCount by remember {
derivedStateOf { items.count { !it.done } }
}
LazyColumn {
items(items, key = { it.id }) { item ->
TodoItem(text = item.text, done = item.done, onToggle = {
viewModel.toggle(item.id)
})
}
}
key 参数给每个条目稳定身份,增删时才不会错位。derivedStateOf 把派生值缓存起来,只在依赖变化时重算,避免每次重组都数一遍。
小结
Compose 状态管理的口诀:remember 管存活、mutableStateOf 管观察、状态提升管复用、StateFlow 管数据、collectAsStateWithLifecycle 管收集。状态放哪取决于多少人要用,怎么变取决于数据从哪来。UI 层保持无状态,数据层收敛进 ViewModel,中间用回调连接,这套架构能撑住从单屏到整个应用的复杂度。