12、Compose状态管理:remember与mutableStateOf、StateFlow与Compose、Compose副作用
状态管理,说白了就是让UI能跟着数据变。在Compose里,这件事做得特别优雅,但也特别容易踩坑。我刚开始从View系统切到Compose时,最不适应的就是「状态到底该放哪」这个问题。今天咱们就把这块彻底捋清楚。
12.1 remember与mutableStateOf:最基础的状态容器
先看一个最简单的例子。你想让一个按钮被点击后,文字从「点我」变成「已点击」。
@Composable
fun ClickCounter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击次数: $count")
}
}
这里有两个关键点:mutableStateOf 创建了一个可观察的状态对象,remember 保证它在重组时不被重置。嗯,这里要注意——remember 只在当前 Composable 的生命周期内有效。如果屏幕旋转或者配置变更,它还是会丢的。
核心理解:
mutableStateOf是数据的「可观察包装」remember是「跨重组保持」的缓存机制- 两者结合,就是Compose中最基础的状态管理单元
我在项目中遇到过一个问题:有个列表的选中状态,我用 remember 存了,结果用户切到后台再回来,选中状态全丢了。为什么?因为 remember 不处理进程重建。这时候就得用 rememberSaveable。
var selectedId by rememberSaveable { mutableStateOf(-1) }
rememberSaveable 会把状态存到 Bundle 里,能扛住配置变更和进程重建。但注意,它只能存基本类型和可序列化的对象。复杂对象你得自己写 Saver。
12.2 StateFlow与Compose:响应式数据流
实际项目中,状态不可能全写在 Composable 里。ViewModel 里管理数据,Compose 里消费数据——这才是正经架构。
我个人习惯用 StateFlow 来桥接 ViewModel 和 Compose。为什么不用 LiveData?因为 StateFlow 是 Kotlin 协程原生支持的,配合 Compose 的 collectAsState() 用起来特别顺。
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun updateName(name: String) {
_uiState.update { it.copy(name = name) }
}
}
data class UiState(
val name: String = "",
val isLoading: Boolean = false
)
@Composable
fun MyScreen(viewModel: MyViewModel) {
val uiState by viewModel.uiState.collectAsState()
Text("当前名字: ${uiState.name}")
}
你想想看,collectAsState() 做了什么?它把 StateFlow 的发射值转成了 Compose 的 State。每次 StateFlow 发新值,Compose 就会自动重组。这就是响应式UI的核心。
个人经验:
我建议在 ViewModel 里用 MutableStateFlow + update() 来更新状态。update() 是原子操作,能避免并发问题。别用 value = xxx 直接赋值,容易出竞态。
还有一个坑:StateFlow 默认是「粘性的」。就是说,如果你在 Composable 里 collect 一个 StateFlow,它会立刻拿到最近一次的值。这通常没问题,但如果你用 SharedFlow 做一次性事件(比如 Snackbar 提示),就得小心了。
// 一次性事件,用 SharedFlow
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
12.3 Compose副作用:LaunchedEffect、DisposableEffect、SideEffect
副作用,就是「在 Composable 之外做的事情」。比如网络请求、数据库操作、注册监听器。Compose 提供了几个专门的副作用函数,用错了会出大问题。
12.3.1 LaunchedEffect:异步任务
想在 Composable 里启动协程?用 LaunchedEffect。
@Composable
fun UserProfile(userId: String) {
var user by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
user = api.fetchUser(userId)
}
Text(user?.name ?: "加载中...")
}
LaunchedEffect 的 key 参数很关键。key 变了,它会取消旧协程,启动新协程。key 不变,它只执行一次。我曾经犯过一个错:把 key 写成了常量,结果 userId 变了也不重新请求——页面一直显示旧数据。
避坑指南:
我曾经在 LaunchedEffect 里直接调用了 ViewModel 的方法,结果 Composable 重组时反复触发。正确的做法是:ViewModel 的方法应该幂等,或者用 remember 做防抖。
12.3.2 DisposableEffect:清理资源
需要注册监听器、传感器、或者 Lifecycle 回调?用 DisposableEffect。它会在离开 Composable 时自动清理。
@Composable
fun LifecycleObserverEffect(onResume: () -> Unit) {
DisposableEffect(Unit) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_RESUME) {
onResume()
}
}
val lifecycle = LocalLifecycleOwner.current.lifecycle
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
}
注意 onDispose 里的清理逻辑。不清理的话,Composable 销毁了监听器还在,轻则内存泄漏,重则崩溃。
12.3.3 SideEffect:同步副作用
SideEffect 在每次重组成功后会执行。我很少用它,除非需要把 Compose 的状态同步到非 Compose 的世界里。
@Composable
fun AnalyticsScreen(screenName: String) {
SideEffect {
Analytics.trackScreen(screenName)
}
}
但注意,SideEffect 不能做异步操作。它就是个同步回调。
12.4 知识体系总览
下面这张图,把今天讲的核心概念串起来了。你仔细看,其实就三层:状态定义、状态提升、副作用处理。
12.5 实战建议
说了这么多,总结几条我实际项目里的经验:
- 简单页面:用
remember + mutableStateOf就够了,别过度设计 - 中等复杂度:ViewModel + StateFlow,配合
collectAsState() - 复杂页面:考虑用
StateFlow做单向数据流,配合sealed class定义 UI 状态 - 一次性事件:用
SharedFlow,别用 StateFlow
一个小技巧:
我习惯在 ViewModel 里定义一个 UiState data class,把所有页面状态都放进去。这样 Compose 里只需要 collect 一个 StateFlow,UI 逻辑特别清晰。重构时也方便,加字段就行,不用改接口。
好了,状态管理这块其实不难,关键是理解「状态在哪定义、怎么传递、副作用怎么处理」这三个问题。你写几个页面练练手,很快就能找到感觉。
公众号:蓝海资料掘金营,微信deep3321