状态管理基础:remember、mutableStateOf的正确使用
做Compose开发,状态管理可以说是最核心的一环。很多重组抖动的问题,追根溯源都是状态用错了地方。我个人习惯把状态管理看作「数据的生命周期管理」——你知道数据什么时候创建、什么时候更新、什么时候被丢弃,自然就不会出乱子。
remember:不是用来做缓存的,是用来绑定生命周期的
很多人把remember当成缓存工具,觉得它能存个值、避免重复计算。这其实是个误解。remember真正的作用,是把一个对象「绑定」到当前Composable的生命周期上。
说白了,Composable函数可能会被反复执行(重组),但remember保证:只要Composable还在组合树里,它返回的值就不会变。一旦Composable从树中移除,remember里的数据才会被回收。
我在项目中遇到过一个问题:一个网络请求结果用remember保存,但用户切换到其他页面再回来,发现数据还在。为什么会这样?因为remember绑定的Composable生命周期还没结束啊,它只是暂时不可见罢了。
mutableStateOf:唯一能触发重组的信号源
如果你想让状态变化后UI自动更新,必须在State对象上做文章。mutableStateOf创建的就是一个可观察的State容器。当它的value变化时,Compose会知道「这里有东西变了」,然后触发重组。
但有个坑我踩过好多次:明明用了mutableStateOf,UI就是不刷新。你猜为什么?因为我把mutableStateOf放在了Composable函数外部,或者用var包了一层,结果Compose根本不关心它的变化。
// ❌ 错误:Compose无法感知外部普通变量的变化
var count = 0
// ✅ 正确:mutableStateOf创建可观察状态
val count = remember { mutableStateOf(0) }
// ✅ 最佳实践:使用by委托,简化语法
var count by remember { mutableStateOf(0) }
记住,mutableStateOf必须放在Composable内部,并且让remember来持有它,Compose才能追踪到变化。否则,你只是在外部改了一个普通变量,UI当然不会理你。
状态管理的常见错误清单
我整理了一个表格,把平时最容易翻车的几个场景列出来。你可以对照排查自己的代码:
| 问题 | 错误写法 | 正确写法 | 后果 |
|---|---|---|---|
| N倍重组 | mutableStateOf定义在循环或列表内部 | 使用LazyColumn时,状态提升到列表外部 | 每次滑动都创建新State,触发无限重组 |
| 对象重建 | 使用mutableStateOf存一个自定义对象 | 使用mutableStateListOf或mutableStateMapOf | 对象引用不变时,Compose认为没有变化 |
| 作用域错误 | 在remember里初始化ViewModel | 使用viewModel()或di注入 | ViewModel随Composable销毁,状态丢失 |
| 作用域错误 | 在remember里执行耗时操作 | 使用LaunchedEffect或rememberCoroutineScope | 重组时重复执行耗时任务,导致卡顿 |
| 监听粒度 | mutableStateOf包裹整个数据类 | 拆分成多个独立State字段 | 改任意字段都触发全量重组,性能爆炸 |
嗯,表格里第一条「N倍重组」我印象最深。有一次我在一个列表里直接创建了mutableStateOf,结果列表有100项,每项都会在滑动时重建。你想想看,100个State同时变化,Compose重组机制直接崩溃。后来我把它提升到列表外部,用LazyColumn配合remember,才解决问题。
正确使用remember的几种场景
既然remember是用来绑定生命周期的,那什么数据适合放在里面?我按实践经验总结了三类:
- 计算结果:比如从列表过滤出符合条件的子集,用
derivedStateOf配合remember - 对象实例:比如正则表达式、格式化器、AnimationSpec等无状态对象
- lambda闭包:如果用
remember保存回调,可以避免不必要的重组
mutableStateOf的拆分配置
我曾经遇到过一个问题:一个Composable里塞了十几个state字段,每次改其中一个,整个Composable都重组。这显然没必要。
正确的做法是:把无关的状态拆开,放到不同的Composable里。每个Composable只负责一块数据,这样改文本字段时,滚动状态就不受影响。
// ❌ 过度耦合:一个Composable管理多个无关状态
@Composable
fun BadForm() {
var name by remember { mutableStateOf("") }
var age by remember { mutableStateOf(0) }
var listState = rememberLazyListState()
// 改name时,listState和age对应的UI也会重组
}
// ✅ 合理拆分:每个状态交给自己的Composable管理
@Composable
fun NameField() {
var name by remember { mutableStateOf("") }
// 只有改name才会重组这里
}
@Composable
fun AgeField() {
var age by remember { mutableStateOf(0) }
// 只有改age才会重组这里
}
这样一拆分,重组的范围就缩小了。你想想看,用户输入姓名时,年龄的字段根本不需要刷新。这就是状态粒度控制的价值。
派生状态:别再手动计算了
还有一个最佳实践是:能用derivedStateOf就别手动算。比如你需要根据两个状态计算出第三个值,直接用derivedStateOf包一下,Compose会自动跟踪依赖,只在依赖变化时才重新计算。
// ❌ 手动计算:每次重组都执行
val isFormValid = name.isNotEmpty() && age > 0
// ✅ 派生状态:只在name或age变化时才重新计算
val isFormValid by remember { derivedStateOf { name.isNotEmpty() && age > 0 } }
这样做的好处是,即使整个Composable因为别的原因重组成千上万次,derivedStateOf也只会在依赖变化时重新执行。性能提升非常明显。
为什么说状态管理是重组的「开关」?
Compose重组的触发条件其实很简单:只有mutableStateOf的值变化了,或传入的参数变了,才会触发重组。其他一切操作(比如普通的var、val、外部变量)都不会触发。
所以,如果你发现UI不更新,先检查两件事:
- 状态是不是mutableStateOf创建的?
- remember有没有正确地绑定到生命周期?
这两条踩住了,80%的重组问题都能找到根源。剩下的,就是作用域和数据流向的问题了。
这张图的核心逻辑很简单:Composable执行时,remember把状态绑定到它的生命周期,mutableStateOf让状态变得可观察。状态变化后Compose检测到,触发重组。派生状态和副作用钩子(LaunchedEffect)是辅助工具,帮你把逻辑拆干净。
我个人习惯写代码前先画一张这样的图。确定每个状态的生命周期、归属的Composable、以及变化后会影响到哪些UI。画清楚了再写代码,重组抖动基本不会找上门。