17、Flow与Compose:Compose中的Flow收集、StateFlow与Compose状态、副作用处理
说实话,Flow 和 Compose 的搭配,是我这两年做 Android 开发最爽的组合之一。以前用 LiveData 总觉得差点意思,后来换成 StateFlow + Compose,才真正体会到什么叫「响应式 UI」。今天咱们就聊聊这个组合怎么用,以及我踩过的那些坑。
Compose 中收集 Flow 的三种姿势
在 Compose 里收集 Flow,说白了就是把数据流转换成 UI 状态。我常用的有三种方式,每种都有自己的适用场景。
1. collectAsState() —— 最省心的方式
这是最直接的方式。你只需要在 Composable 函数里调用 .collectAsState(),Flow 发射的数据就会自动变成 Compose 的 State。UI 会自动重组,不需要你手动处理生命周期。
@Composable
fun UserProfile(userId: String) {
val viewModel: UserViewModel = viewModel()
// 直接收集,自动转成 State
val userState by viewModel.getUserFlow(userId)
.collectAsState(initial = User())
Text("用户名: ${userState.name}")
Text("邮箱: ${userState.email}")
}
嗯,这里要注意一点:collectAsState() 默认会在 Composition 的 生命周期内 持续收集。也就是说,当 Composable 退出组合时,收集会自动停止。这一点比 LiveData 要优雅得多。
2. collectAsStateWithLifecycle() —— 更安全的收集
这个 API 是 lifecycle-runtime-compose 库提供的。它比 collectAsState() 多了一层生命周期感知。什么意思呢?
举个例子:你的 App 退到后台了,collectAsState() 可能还在收集数据,但 UI 已经不可见了。而 collectAsStateWithLifecycle() 会在 Lifecycle 低于 STARTED 时停止收集,回到前台再恢复。
@Composable
fun UserProfile(userId: String) {
val viewModel: UserViewModel = viewModel()
// 感知生命周期,后台自动停止收集
val userState by viewModel.getUserFlow(userId)
.collectAsStateWithLifecycle(
initialValue = User(),
minActiveState = Lifecycle.State.STARTED
)
// ... UI 代码
}
3. collectInComposition() —— 处理副作用
有时候你不需要把 Flow 的数据映射成 UI 状态,而是想在 Flow 发射数据时执行一些副作用操作,比如显示 Snackbar、记录日志、触发导航等。这时候可以用 collectInComposition()。
@Composable
fun LoginScreen(viewModel: LoginViewModel) {
// 收集一次性事件,比如导航到主页
LaunchedEffect(Unit) {
viewModel.navigationEvent.collect { event ->
when (event) {
is NavigationEvent.GoToHome -> {
// 执行导航
}
is NavigationEvent.ShowError -> {
// 显示错误提示
}
}
}
}
}
说白了,collectInComposition() 就是 Compose 版的 repeatOnLifecycle。它会在 Composition 的生命周期内收集 Flow,适合处理那些「一次性」的事件。
StateFlow 与 Compose 状态:天生一对
StateFlow 和 Compose 的 State 本质上是一回事。StateFlow 是 Kotlin 协程层面的状态容器,Compose 的 State 是 UI 层面的状态容器。两者之间就差一个 collectAsState() 的转换。
我个人习惯在 ViewModel 里用 MutableStateFlow 管理状态,然后通过 asStateFlow() 暴露不可变的 StateFlow 给 UI 层。
class UserViewModel : ViewModel() {
// 私有可变状态
private val _userState = MutableStateFlow(User())
// 公开不可变状态
val userState: StateFlow<User> = _userState.asStateFlow()
fun updateUser(name: String) {
_userState.update { it.copy(name = name) }
}
}
你想想看,这样做的好处是什么?ViewModel 里的状态变化是响应式的,Compose 那边只要收集就能自动更新。而且 StateFlow 天然支持协程,可以跟其他 Flow 操作符无缝配合。
副作用处理:LaunchedEffect 与 rememberCoroutineScope
在 Compose 里处理 Flow 的副作用,我主要用两个工具:LaunchedEffect 和 rememberCoroutineScope。
LaunchedEffect —— 进入组合时启动协程
LaunchedEffect 会在 Composable 首次进入组合时启动一个协程,并在退出组合时自动取消。它最适合用来收集那些「跟 UI 生命周期绑定」的 Flow。
@Composable
fun TimerScreen() {
val time = remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
// 每秒更新一次时间
flow {
while (true) {
emit(Unit)
delay(1000)
}
}.collect {
time.value += 1
}
}
Text("已过去 ${time.value} 秒")
}
这里有个细节:LaunchedEffect 的 key 参数很重要。如果 key 变化了,旧的协程会被取消,新的协程会重新启动。我经常用这个特性来做「重新加载」的逻辑。
rememberCoroutineScope —— 在事件回调中启动协程
有时候你需要在用户点击按钮时启动一个协程,而不是在 Composable 进入组合时。这时候 rememberCoroutineScope 就派上用场了。
@Composable
fun RefreshButton(viewModel: MyViewModel) {
val scope = rememberCoroutineScope()
Button(onClick = {
// 用户点击时启动协程
scope.launch {
viewModel.refreshData()
}
}) {
Text("刷新")
}
}
嗯,这里要注意:rememberCoroutineScope 返回的 CoroutineScope 是跟当前 Composable 的生命周期绑定的。当 Composable 退出组合时,这个 scope 里的所有协程都会被取消。所以不用担心内存泄漏。
知识体系总览
下面这张图总结了 Flow 与 Compose 的核心关系,我把它画成了 SVG,方便你理解整体脉络。
避坑指南:我踩过的几个坑
最后分享几个实战中容易踩的坑,希望能帮你少走弯路。
- 不要在 Composable 里直接调用 Flow 的 collect() —— 这会导致协程泄漏。一定要用
LaunchedEffect或collectAsState()来管理协程生命周期。 - StateFlow 的 initial value 不要设成 null —— 我见过很多新手用
MutableStateFlow<User?>(null),结果 UI 里到处都是空判断。建议用 sealed class 或者默认值。 - SharedFlow 的 replay 参数要小心 —— 如果你用 SharedFlow 做事件总线,replay 设成 0 是「一次性事件」,设成 1 就是「粘性事件」。我建议事件用 SharedFlow(replay=0),状态用 StateFlow。
- collectAsStateWithLifecycle() 的 minActiveState 默认是 STARTED —— 如果你希望后台也收集数据(比如下载进度),可以改成 CREATED。但大多数场景下 STARTED 就够了。
好了,今天就聊到这里。Flow 和 Compose 的组合拳还有很多玩法,比如用 flatMapLatest 做搜索防抖、用 combine 合并多个状态源……这些咱们后面再细聊。
公众号:蓝海资料掘金营,微信deep3321