9、状态管理(下):StateFlow与collectAsState、rememberSaveable、Compose中的副作用
好,我们接着聊状态管理。上一章我们把 Compose 里那些基础的状态工具捋了一遍,什么 mutableStateOf、remember,还有状态提升。今天要聊的这几个,说实话,才是真正让你在项目里写出“能打”代码的关键。
我个人习惯把这一章的内容分成两块:一块是 跨页面/跨线程的状态流,另一块是 跟生命周期绑定的副作用。这两块搞明白了,你写 Compose 的时候就不会再问“为什么我的数据丢了?”或者“为什么我的协程还在跑?”这种问题了。
StateFlow 与 collectAsState
先说说 StateFlow。这东西说白了就是 Kotlin 协程家族里专门为“状态”设计的 Flow。它跟普通的 Flow 最大的区别是:它永远持有最新的值,而且当有新的订阅者时,它会立刻把当前值发出去。
我在项目中遇到过这样一个场景:用户登录后,我们需要在多个页面显示用户头像和昵称。如果用普通的 LiveData 或者 mutableStateOf,你得在各个页面手动同步。但用 StateFlow,我只需要在 ViewModel 里暴露一个 StateFlow<UserInfo>,所有页面通过 collectAsState 收集,数据自动同步。
核心要点:StateFlow 是热流,它不依赖收集者也能独立工作。而 collectAsState 是 Compose 专门为 StateFlow 提供的“桥接函数”,它会把 StateFlow 转换成 Compose 可观察的 State。
来看代码:
// ViewModel 中
class ProfileViewModel : ViewModel() {
private val _userInfo = MutableStateFlow(UserInfo())
val userInfo: StateFlow<UserInfo> = _userInfo.asStateFlow()
fun loadUser() {
viewModelScope.launch {
val result = repository.fetchUser()
_userInfo.value = result
}
}
}
// Composable 中
@Composable
fun ProfileScreen(viewModel: ProfileViewModel) {
val userInfo by viewModel.userInfo.collectAsState()
Text("用户名:${userInfo.name}")
}
嗯,这里要注意一点:collectAsState 默认会在 Composable 的生命周期内持续收集。如果你在后台页面,它也会继续收集,但不会触发重组。这个行为是 Compose 帮我们优化好的,你不需要额外操心。
我的小技巧:如果你在 ViewModel 里用 MutableStateFlow,记得用 .asStateFlow() 暴露出去。这样外部就只能读不能写,避免数据被意外篡改。我曾经在一个团队项目里没注意这个,结果某个同事直接在 Activity 里改了 StateFlow 的值,排查了半天……
rememberSaveable:让状态扛过配置变更
接下来聊 rememberSaveable。这个函数跟 remember 长得像,但功能强大多了。它不光能记住状态,还能在 Activity 重建(比如屏幕旋转、语言切换)后把状态恢复回来。
为什么会这样?因为 rememberSaveable 内部用了 SavedStateHandle,它会把数据序列化到 Bundle 里。所以,你存进去的东西必须能序列化。基本类型、String、甚至 Parcelable 都没问题,但如果你存了一个 Lambda 或者一个复杂的对象,那就得小心了。
我记得有一次,我在一个搜索框里用了 remember 来保存输入的关键词。结果用户旋转屏幕后,关键词丢了,用户气得差点摔手机。后来换成 rememberSaveable,问题就解决了。
@Composable
fun SearchBox() {
var query by rememberSaveable { mutableStateOf("") }
TextField(
value = query,
onValueChange = { query = it },
placeholder = { Text("输入搜索关键词") }
)
}
避坑指南:我曾经把一个自定义数据类直接塞进 rememberSaveable,结果崩溃了。后来才发现,自定义类必须实现 Parcelable 或者用 @Parcelize 注解。如果你不想写序列化代码,可以考虑用 mapSaver 或者 listSaver 来手动保存。
Compose 中的副作用:LaunchedEffect 与 DisposableEffect
副作用,说白了就是“跟 UI 无关但必须做的事”。比如发起网络请求、订阅传感器、启动定时器。Compose 里提供了几个专门的 Effect 函数来管理这些操作。
LaunchedEffect:在 Composable 中启动协程
LaunchedEffect 会在 Composable 进入组合时启动一个协程,并在离开组合时自动取消。它的 key 参数很关键:当 key 变化时,旧的协程会被取消,新的协程会启动。
我个人的习惯是:只要需要在 Composable 里做异步操作,第一反应就是用 LaunchedEffect。比如加载初始数据:
@Composable
fun UserProfile(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
userData = repository.fetchUser(userId)
}
if (userData != null) {
Text("用户:${userData.name}")
} else {
CircularProgressIndicator()
}
}
你看,当 userId 变化时,旧的请求会被取消,新的请求自动发起。这比在 ViewModel 里手动管理协程要简洁得多。
注意:LaunchedEffect 的协程作用域是 rememberCoroutineScope(),但它会自动管理生命周期。如果你需要在 Composable 外部(比如点击事件)启动协程,那就用 rememberCoroutineScope() 手动创建作用域。
DisposableEffect:需要清理的副作用
有些副作用需要明确的清理操作,比如取消注册监听器、关闭数据库连接。这时候 DisposableEffect 就派上用场了。它会在 Composable 离开组合时执行清理代码。
我曾经在做一个传感器应用时,需要在 Composable 里注册一个加速度传感器监听器。如果忘记取消注册,Activity 销毁后传感器还在跑,不仅耗电,还会导致内存泄漏。用 DisposableEffect 就能完美解决:
@Composable
fun SensorDisplay(sensorManager: SensorManager) {
DisposableEffect(sensorManager) {
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) {
// 处理传感器数据
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
sensorManager.registerListener(listener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
onDispose {
sensorManager.unregisterListener(listener)
}
}
}
嗯,这里的关键是 onDispose 块。它里面的代码一定会在 Composable 从组合中移除时执行,不管是因为导航离开、屏幕旋转还是其他原因。
知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图。它把状态管理和副作用的核心逻辑串起来了:
总结一下
这一章的内容其实就三个核心点:
- StateFlow + collectAsState:解决跨页面、跨线程的状态同步问题。记住用
.asStateFlow()保护数据。 - rememberSaveable:让你的状态在屏幕旋转等配置变更后依然存活。注意序列化问题。
- LaunchedEffect 和 DisposableEffect:把副作用交给 Compose 管理,别自己瞎搞协程生命周期。
说实话,这些工具刚接触时可能会觉得有点绕,但用多了你会发现,它们其实是在帮你减少犯错的机会。你想想看,如果没有 DisposableEffect,你每次都得在 onStop 或 onDestroy 里手动清理,多容易漏啊。
好了,这一章就到这里。代码不多,但每个都是实战中高频使用的。你可以在自己的项目里试着把 remember 换成 rememberSaveable,把 LiveData 换成 StateFlow,感受一下区别。嗯,相信我,你会爱上这种“再也不用担心状态丢失”的感觉的。