6、状态管理(下):StateFlow 与 collectAsState,Compose 中的副作用
好,咱们接着聊状态管理。上一章我们把 Compose 里的基础状态讲透了,这一章要上点硬菜——StateFlow 和 副作用。这两个东西,说白了就是帮你把「异步数据」和「生命周期」管明白的。
我个人习惯把 StateFlow 理解成「升级版的 LiveData」。为什么这么说?因为它天生支持协程,而且没有 LiveData 那些奇奇怪怪的粘性事件问题。我在项目中遇到过好几次,用 LiveData 做搜索框时,屏幕旋转后老数据又冒出来了,烦得很。换成 StateFlow 后,世界清净了。
6.1 StateFlow 与 collectAsState
先看一个最简单的例子。假设你有一个 ViewModel,里面有个倒计时功能:
class TimerViewModel : ViewModel() {
private val _seconds = MutableStateFlow(0)
val seconds: StateFlow<Int> = _seconds.asStateFlow()
fun startTimer() {
viewModelScope.launch {
while (true) {
delay(1000)
_seconds.value++
}
}
}
}
在 Compose 里怎么用?一句话的事:
@Composable
fun TimerScreen(viewModel: TimerViewModel = viewModel()) {
val seconds by viewModel.seconds.collectAsState()
Text("已过去:$seconds 秒")
}
嗯,这里要注意:collectAsState() 默认会帮你处理生命周期。屏幕可见时收集,不可见时自动取消。你想想看,这比手写 LiveData 的 observe 省了多少事?
6.1.1 为什么不用 LiveData?
我曾经在项目里做过一个对比测试。同样的数据流,LiveData 在配置变更后会有 200ms 左右的「粘性事件」窗口期。而 StateFlow 因为基于协程,天然就是冷流 + 去重设计。
说白了,StateFlow 有三大优势:
- 无粘性事件:新订阅者不会收到旧数据
- 协程友好:可以跟 Flow 的各种操作符无缝衔接
- 去重:只有值真正变化时才通知,避免无谓重组
stateIn(SharingStarted.WhileSubscribed(5000)) 替代 MutableStateFlow。
6.2 Compose 中的副作用
副作用这个词,听着挺唬人。其实说白了就是:在 Composable 函数里干「不纯」的事。比如网络请求、数据库写入、注册监听器——这些都不能直接写在 Composable 函数体里,因为 Compose 会反复执行。
那怎么办?官方给了三个宝贝:LaunchedEffect、DisposableEffect、rememberCoroutineScope。
6.2.1 LaunchedEffect:启动协程,自动取消
这个最常用。它会在 Composable 进入组合时启动一个协程,离开时自动取消。我习惯用它来做「一次性」的初始化工作:
@Composable
fun UserProfile(userId: String) {
var userData by remember { mutableStateOf<User?>(null) }
LaunchedEffect(userId) {
// 当 userId 变化时,重新请求
userData = api.fetchUser(userId)
}
// 渲染 UI...
}
注意看,我把 userId 作为 key 传给了 LaunchedEffect。这意味着:只有 userId 变化时,协程才会重启。如果 userId 没变,即使重组也不会重新执行。
6.2.2 DisposableEffect:有借有还
这个跟 LaunchedEffect 很像,但多了一个「清理」回调。适合注册/反注册的场景:
@Composable
fun SensorDisplay() {
val context = LocalContext.current
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
DisposableEffect(Unit) {
val listener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent?) { /* 处理数据 */ }
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}
sensorManager.registerListener(listener, ...)
// 这个 lambda 就是清理函数
onDispose {
sensorManager.unregisterListener(listener)
}
}
}
你想想看,如果不用 DisposableEffect,你很可能忘记在 Composable 离开时取消注册。那就会造成内存泄漏。嗯,我早期做项目时就吃过这个亏,后来养成了习惯:只要涉及注册,就用 DisposableEffect。
6.2.3 rememberCoroutineScope:手动控制协程
有时候你需要在 Composable 外部启动协程,比如按钮点击事件里。这时候就需要 rememberCoroutineScope:
@Composable
fun LoginScreen() {
val scope = rememberCoroutineScope()
var loading by remember { mutableStateOf(false) }
Button(
onClick = {
scope.launch {
loading = true
try {
loginApi.login()
} finally {
loading = false
}
}
}
) {
Text(if (loading) "登录中..." else "登录")
}
}
这里有个细节:rememberCoroutineScope 返回的 scope 会跟随 Composable 的生命周期。如果 Composable 被移出组合,scope 会自动取消所有未完成的协程。所以不用担心泄漏问题。
- 需要自动跟随生命周期?用 LaunchedEffect
- 需要清理资源?用 DisposableEffect
- 需要手动控制时机(比如点击事件)?用 rememberCoroutineScope
6.3 知识体系总览
下面这张图,是我自己梳理的「状态管理 + 副作用」知识结构。你可以把它当作一个速查表:
6.4 实战:一个完整的搜索页面
最后,我把上面这些知识点串起来,写一个搜索页面的核心逻辑。你感受一下:
class SearchViewModel : ViewModel() {
private val _query = MutableStateFlow("")
val query: StateFlow<String> = _query.asStateFlow()
private val _results = MutableStateFlow<List<String>>(emptyList())
val results: StateFlow<List<String>> = _results.asStateFlow()
fun onQueryChanged(newQuery: String) {
_query.value = newQuery
}
fun search() {
viewModelScope.launch {
_results.value = api.search(_query.value)
}
}
}
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
val query by viewModel.query.collectAsState()
val results by viewModel.results.collectAsState()
Column(modifier = Modifier.padding(16.dp)) {
OutlinedTextField(
value = query,
onValueChange = { viewModel.onQueryChanged(it) },
label = { Text("搜索") }
)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { viewModel.search() }) {
Text("搜索")
}
// 防抖处理:用户停止输入 500ms 后自动搜索
LaunchedEffect(query) {
if (query.isBlank()) return@LaunchedEffect
delay(500)
viewModel.search()
}
LazyColumn {
items(results) { item ->
Text(item, modifier = Modifier.padding(8.dp))
}
}
}
}
你看,这里用到了:
- StateFlow 管理查询词和结果
- collectAsState 在 Compose 中收集
- LaunchedEffect 实现防抖
- rememberCoroutineScope 其实没用到——因为搜索按钮直接调用了 ViewModel 的方法,不需要手动启动协程
好了,这一章的内容就到这里。StateFlow 和副作用 API 是 Compose 开发中的高频知识点,建议你多写几个小例子练练手。尤其是 DisposableEffect 的清理逻辑,很容易漏掉——我曾经就因为忘了在 onDispose 里取消网络请求,导致页面切走后还在回调里更新 UI,直接崩溃了。从那以后,我写任何注册逻辑都会条件反射地加上 onDispose。
公众号:蓝海资料掘金营,微信deep3321