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 要优雅得多。

我的习惯: 如果只是简单的数据展示,我基本都用 collectAsState()。它最轻量,代码也最简洁。

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 代码
}
我曾经踩过的坑: 有一次用 collectAsState() 在后台持续收集网络数据,结果用户切到后台再回来,发现界面闪了一下。因为后台收集的数据没被 UI 消费,积压了一批状态,回来时一次性全重组了。换成 collectAsStateWithLifecycle() 就解决了。

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 操作符无缝配合。

核心要点: StateFlow 是「数据驱动 UI」的最佳实践。它保证了状态的一致性,避免了 LiveData 那种「粘性事件」的问题。

副作用处理:LaunchedEffect 与 rememberCoroutineScope

在 Compose 里处理 Flow 的副作用,我主要用两个工具:LaunchedEffectrememberCoroutineScope

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 里的所有协程都会被取消。所以不用担心内存泄漏。

我的建议: 能用 LaunchedEffect 就用 LaunchedEffect,它更安全。只有在需要「事件触发」的场景下才用 rememberCoroutineScope。

知识体系总览

下面这张图总结了 Flow 与 Compose 的核心关系,我把它画成了 SVG,方便你理解整体脉络。

Flow + Compose 核心关系图 ViewModel MutableStateFlow StateFlow SharedFlow ┄┄┄┄┄┄┄┄┄┄┄┄┄ 业务逻辑 数据转换 网络请求 生命周期:ViewModelScope collectAsState() Compose UI State<T> collectAsState() collectAsStateWithLifecycle() ┄┄┄┄┄┄┄┄┄┄┄┄┄ LaunchedEffect rememberCoroutineScope 重组 生命周期:Composition 事件/操作 用户事件 点击 滑动 输入 ┄┄┄┄┄┄┄┄ 导航 Snackbar 日志 数据流方向:ViewModel → Compose UI(单向数据流) 事件流方向:用户操作 → ViewModel(通过函数调用)

避坑指南:我踩过的几个坑

最后分享几个实战中容易踩的坑,希望能帮你少走弯路。

  • 不要在 Composable 里直接调用 Flow 的 collect() —— 这会导致协程泄漏。一定要用 LaunchedEffectcollectAsState() 来管理协程生命周期。
  • 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 负责 UI 呈现,两者通过 collectAsState() 这座桥连接。记住「单向数据流」这个原则,你的代码就会清晰很多。

好了,今天就聊到这里。Flow 和 Compose 的组合拳还有很多玩法,比如用 flatMapLatest 做搜索防抖、用 combine 合并多个状态源……这些咱们后面再细聊。


公众号:蓝海资料掘金营,微信deep3321