5、状态管理(上):remember 与 mutableStateOf,状态提升(State Hoisting),ViewModel 集成
状态管理,说白了就是解决「数据从哪里来、到哪里去、怎么变」的问题。我在做传统 Android 开发时,经常被 Activity 的 onSaveInstanceState 折腾得够呛。到了 Compose 时代,这套思路完全变了——你不再需要手动保存恢复状态,而是用声明式的方式告诉 UI:「数据变了,你重新画一遍就行。」
嗯,这一章我们先把最核心的三个概念讲透:remember、状态提升、ViewModel 集成。它们就像三块积木,搭好了,你的应用状态就不会乱成一锅粥。
5.1 remember 与 mutableStateOf:最基础的状态容器
先看一个最简单的例子。你想在界面上显示一个数字,点一下按钮就加一。在 Compose 里怎么写?
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击了 $count 次")
}
}
这里有两个关键点。第一,mutableStateOf(0) 创建了一个可观察的状态容器。第二,remember 保证这个容器在重组时不会被重新创建。你想想看,如果没有 remember,每次重组都会 new 一个新的 count,那界面永远都是 0。
核心理解:remember 是「记住」,mutableStateOf 是「可观察」。两者缺一不可。
我在项目中遇到过一个问题:有个同事在 Composable 函数里直接写了 var count = 0,然后 onClick 里 count++。结果界面死活不更新。为什么?因为普通变量变了,Compose 不知道啊。必须用 mutableStateOf 包装一下,Compose 才能感知到变化并触发重组。
小技巧:用 by 委托可以少写 .value,代码更清爽。如果你不喜欢委托,也可以写成 val count = remember { mutableStateOf(0) },然后通过 count.value 读写。
5.2 状态提升(State Hoisting):把状态「往上提」
刚才的 Counter 例子,状态 count 是定义在 Composable 内部的。这叫「局部状态」。但实际项目中,你经常需要多个组件共享同一个状态。这时候就要用状态提升。
说白了,状态提升就是把状态从子组件「提」到父组件,然后通过参数传下来。子组件只负责「通知」父组件状态要变了,不负责「存储」状态。
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Column {
CounterDisplay(count = count)
CounterButton(onIncrement = { count++ })
}
}
@Composable
fun CounterDisplay(count: Int) {
Text("当前值:$count")
}
@Composable
fun CounterButton(onIncrement: () -> Unit) {
Button(onClick = onIncrement) {
Text("加一")
}
}
你看,CounterDisplay 和 CounterButton 都不持有状态。它们一个只读,一个只通知。状态全部集中在 CounterScreen 里。这就是「单一数据源」原则。
我曾经踩过的坑:一开始我把状态放在子组件里,结果两个子组件需要同步同一个数据,我只能用回调一层层往上抛,代码变得又臭又长。后来统一用状态提升,清爽多了。记住:能往上提就往上提,别让状态「下沉」。
状态提升的好处很明显:
- 可测试性:子组件变成纯 UI 组件,传入什么就显示什么,单元测试好写
- 可复用性:CounterDisplay 可以在任何地方用,只要传一个 Int 给它
- 可维护性:状态变化路径清晰,不会出现「这个值到底是谁改的」这种问题
5.3 ViewModel 集成:让状态活得更久
remember 有个问题:它只在当前 Composable 在组合树中的时候有效。如果 Activity 重建(比如屏幕旋转),remember 的状态就丢了。你想想看,用户填了一半的表单,转一下屏幕全没了,这体验能忍吗?
这时候 ViewModel 就派上用场了。ViewModel 的生命周期比 Composable 长,它不受重组和配置变更的影响。
class CounterViewModel : ViewModel() {
private val _count = mutableStateOf(0)
val count: State<Int> = _count
fun increment() {
_count.value++
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count = viewModel.count
Column {
Text("当前值:$count")
Button(onClick = { viewModel.increment() }) {
Text("加一")
}
}
}
这里有几个细节要注意。第一,ViewModel 里用 mutableStateOf 而不是 LiveData 或 Flow。为什么?因为 Compose 原生支持 State,不需要额外转换。第二,viewModel() 这个函数是 Compose 提供的,它会自动帮你获取或创建 ViewModel,不需要手动传 Factory。
最佳实践:ViewModel 负责业务逻辑和状态持有,Composable 只负责渲染。ViewModel 里不要持有 Composable 的引用,也不要有任何 UI 相关的代码。
我个人习惯把 ViewModel 的接口设计成「只读状态 + 操作函数」的形式。外部只能读状态,不能直接改。这样数据流向是单向的,不容易出 bug。
| 特性 | remember | ViewModel |
|---|---|---|
| 生命周期 | 跟随 Composable | 跟随 Activity/Fragment |
| 配置变更 | 丢失 | 保留 |
| 作用域 | 局部 | 全局(可共享) |
| 适用场景 | 临时 UI 状态(如展开/收起) | 业务状态(如用户信息、列表数据) |
我的建议:简单的 UI 状态(比如某个弹窗是否显示)用 remember 就够了。需要持久化或跨组件共享的状态,果断上 ViewModel。别为了省事把所有状态都塞进 ViewModel,也别为了「简洁」把业务状态放在 remember 里。
嗯,这一章的内容就到这里。记住三个关键词:remember 让状态「活」在重组中,状态提升让数据流向清晰,ViewModel 让状态「活」过配置变更。这三板斧用好了,你的 Compose 应用状态管理就稳了。
公众号:蓝海资料掘金营,微信deep3321