彻底搞懂 Compose 重组与状态管理
刚从传统 View 系统切到 Jetpack Compose 的同学,通常都会遇到两类非常典型的问题:
- 点了按钮,变量明明变了,界面却不刷新。
- 页面终于能用了,一旋转屏幕,刚输入的数据又全没了。
这两个问题看起来不同,但核心都指向同一个主题:状态放在哪里,以及 Compose 什么时候会重新执行 UI。
一、Compose 为什么和传统 View 不一样
在传统 View 系统中,控件自己持有状态:
TextView持有文本CheckBox持有勾选状态EditText持有输入内容
而 Compose 是声明式 UI。你不再“修改控件本身”,而是通过修改数据,让 UI 根据最新状态重新计算:
UI = f(State)
也就是说,UI 只是状态的函数结果。
二、为什么“点了按钮没反应”
最常见的错误写法大概是这样:
@Composable
fun BadCounter() {
var count = 0
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Add")
}
}
}
点击按钮后,count 的值确实变了,但界面不刷新。
原因是:count 只是一个普通 Kotlin 局部变量,Compose 根本不知道它发生了变化。
正确做法:使用 mutableStateOf
@Composable
fun BetterCounter() {
var count by mutableStateOf(0)
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Add")
}
}
}
但这段代码其实还是有问题。因为每次重组时,这个函数都会重新执行,count 又会被初始化成 0。
三、remember 到底解决了什么
要让状态在重组之间保留下来,必须使用 remember:
@Composable
fun GoodCounter() {
var count by remember { mutableStateOf(0) }
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Add")
}
}
}
remember 的作用可以理解为:
- 第一次执行时创建状态
- 后续重组时直接复用之前那份状态
这样 count 就不会因为函数重跑而被重置。
什么时候只用 remember 就够了
适合这类纯页面内的临时状态:
- 展开 / 收起
- Tab 高亮
- 局部选中态
- 短生命周期动画状态
只要页面没被销毁,它就能工作得很好。
四、为什么旋转屏幕后状态又没了
即使你用了 remember,设备一旋转,状态还是会丢。
原因是:旋转屏幕会触发配置变更,默认情况下 Activity 会被销毁再重建。remember 只保证同一轮 Composition 中的重组不丢状态,无法跨 Activity 重建保留数据。
解决方案:rememberSaveable
@Composable
fun ResilientCounter() {
var count by rememberSaveable { mutableStateOf(0) }
Column {
Text(text = "Count: $count")
Button(onClick = { count++ }) {
Text("Add")
}
}
}
rememberSaveable 会借助 Android 的状态保存机制,把可保存的数据放进 Bundle,页面重建后再恢复出来。
它适合什么场景
- 输入框内容
- 页面内选项切换
- 当前分页索引
- 简单表单状态
它不适合什么
如果状态本身是业务核心数据,比如:
- 列表接口返回结果
- 当前登录用户
- 详情页业务对象
那就不应该继续堆在 Composable 内部了。
五、真正可维护的方案:状态上提 + ViewModel
一旦状态不再只是“局部 UI 小开关”,而是业务状态,就应该把它上提到 ViewModel。
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count = _count.asStateFlow()
fun increment() {
_count.value++
}
}
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val count by viewModel.count.collectAsStateWithLifecycle()
CounterContent(
count = count,
onIncrement = viewModel::increment
)
}
@Composable
fun CounterContent(
count: Int,
onIncrement: () -> Unit
) {
Column {
Text(text = "Count: $count")
Button(onClick = onIncrement) {
Text("Add")
}
}
}
这种写法的好处非常明显:
- 状态不怕重组
- 状态也不怕旋转屏幕
- UI 组件变成无状态,复用和测试都更轻松
这就是 Compose 里非常重要的模式:State Hoisting(状态上提)。
六、如何判断状态该放哪
你可以按下面这个思路快速判断:
放在 remember
适合只在当前页面短暂存在、页面销毁就无所谓的数据。
放在 rememberSaveable
适合希望在旋转、重建后仍保留的轻量 UI 状态。
放在 ViewModel
适合需要跨配置变化、跨页面流程、甚至和业务逻辑强耦合的状态。
七、排查 Compose 状态问题的实用顺序
当你发现 Compose 页面“不对劲”时,建议按这个顺序查:
- 这个值是不是普通变量,而不是
mutableStateOf。 - 这个状态有没有被
remember包起来。 - 页面是不是经历了
Activity重建,需要rememberSaveable。 - 这个状态是不是其实已经超出了 UI 层,应该上提到
ViewModel。
八、结论
Compose 本身并不神秘,问题通常出在我们还沿用了传统 View 的思维方式。
只要记住下面这几条,很多“界面不刷新”和“状态莫名消失”的问题都会自然消失:
- UI 只是状态的映射,不要直接试图“改控件”
- 普通变量不会触发重组,要用
mutableStateOf remember解决重组问题,不解决重建问题rememberSaveable解决轻量状态恢复- 业务状态尽早上提到
ViewModel
当状态边界清晰以后,Compose 的开发体验会顺很多,代码结构也会比传统 UI 更稳定、更好维护。