10、StateFlow与SharedFlow:StateFlow与LiveData对比、StateFlow的使用场景、SharedFlow的配置(replay、extraBufferCapacity)、SharedFlow与StateFlow的选择

好,咱们今天来聊聊 Flow 家族里两个非常重要的成员——StateFlow 和 SharedFlow。说实话,这两个东西刚出来的时候,我也纠结过一阵子。尤其是从 LiveData 迁移过来的那段时间,总觉得「嗯?这不就是 LiveData 换了个马甲吗?」。但用久了你会发现,它们俩的定位完全不同,选错了,坑可不小。

StateFlow 与 LiveData 的对比

先说说 StateFlow 和 LiveData 的区别。我个人习惯把 LiveData 看作是「Android 原生时代的产物」,而 StateFlow 是「协程时代的正规军」。为什么这么说?

对比维度 LiveData StateFlow
生命周期感知 原生支持,自动取消订阅 需要配合 repeatOnLifecycle 或 collect
线程安全 主线程更新,postValue 有丢数据风险 协程安全,支持并发更新
初始值 无强制要求 必须提供初始值
粘性事件 默认粘性 StateFlow 粘性,SharedFlow 可配置
操作符支持 有限(map、switchMap 等) 完整的 Flow 操作符链
单元测试 需要 Android 依赖 纯 Kotlin,测试友好

我在项目中遇到过这样一个场景:用 LiveData 做倒计时,结果 postValue 在短时间内多次调用时,中间的值全丢了。你想想看,用户看到倒计时从 10 直接跳到 7,体验多糟糕。换成 StateFlow 配合 MutableStateFlow,用协程的 delay 来更新,问题就解决了。

核心区别一句话:LiveData 是为 View 层设计的观察者模式工具,而 StateFlow 是协程生态中状态管理的标准方案。

StateFlow 的使用场景

StateFlow 说白了就是「带状态的热流」。它始终持有最新的值,新订阅者一进来就能拿到当前状态。我个人觉得它最适合以下场景:

  • UI 状态管理:比如 ViewModel 中的界面状态,用 StateFlow 再合适不过
  • 数据层缓存:比如用户信息、配置参数,需要保持最新值
  • 跨页面共享状态:多个 Fragment 需要读取同一个状态时
// 一个典型的 ViewModel 用法
class MainViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(MainUiState())
    val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()

    fun updateName(name: String) {
        _uiState.update { it.copy(userName = name) }
    }
}

这里有个小细节:用 update 而不是直接 value =。为什么?因为 update 是原子操作,在高并发场景下不会出现竞态条件。我曾经在项目里直接用 value = 赋值,结果在多个协程同时更新时,状态出现了不一致。嗯,从那以后我就只用 update 了。

SharedFlow 的配置:replay 与 extraBufferCapacity

SharedFlow 比 StateFlow 灵活得多。它就像一个「可配置的广播站」,你可以决定:

  • 新来的听众能听到之前几条消息(replay)
  • 消息发太快时,缓存区能存多少条(extraBufferCapacity)
  • 缓存满了怎么办(onBufferOverflow)
// SharedFlow 的典型配置
val sharedFlow = MutableSharedFlow<Int>(
    replay = 1,               // 新订阅者能收到最近 1 条历史消息
    extraBufferCapacity = 5,  // 额外缓存 5 条
    onBufferOverflow = BufferOverflow.DROP_OLDEST  // 满了就丢弃最旧的
)

这三个参数怎么配?我根据自己的经验总结一下:

参数 作用 推荐值
replay 新订阅者能收到多少条历史数据 事件类用 0,状态类用 1
extraBufferCapacity 缓存区大小,防止背压 根据生产速度定,一般 5-10
onBufferOverflow 缓存满时的策略 事件用 DROP_OLDEST,状态用 SUSPEND

避坑指南:我曾经把 replay 设成 100,结果页面切回来时,用户看到了一堆过期的 Toast 提示。后来改成 replay = 0,配合 extraBufferCapacity = 5,才解决了问题。

SharedFlow 与 StateFlow 的选择

这个问题其实很简单,但很多人会纠结。我给大家一个判断标准:

  • 需要「当前值」吗? 需要 → StateFlow;不需要 → SharedFlow
  • 需要「多个独立事件」吗? 需要 → SharedFlow;不需要 → StateFlow
  • 需要「配置粘性行为」吗? 需要 → SharedFlow;默认粘性 → StateFlow

说白了,StateFlow 是 SharedFlow 的一个特例——它相当于 replay = 1,extraBufferCapacity = 0,onBufferOverflow = DROP_OLDEST 的 SharedFlow。但 Google 把它单独拎出来,就是为了语义清晰:StateFlow 代表「状态」,SharedFlow 代表「事件」。

注意:不要把事件(如点击、导航、Toast)用 StateFlow 来发。因为 StateFlow 会保留最新值,用户旋转屏幕后重新订阅,会再次触发这个「事件」,导致重复弹 Toast。我见过有人踩这个坑,调试了半天才发现是 StateFlow 的粘性特性导致的。

知识体系图

下面这张图帮你理清 StateFlow 和 SharedFlow 的关系与选择逻辑:

StateFlow vs SharedFlow 选择决策树 Flow 热流 需要「当前值」吗? StateFlow replay=1, 粘性 SharedFlow 可配置 replay/buffer UI 状态、数据缓存 跨页面共享状态 一次性事件、导航 Toast、Snackbar StateFlow = 状态管理 | SharedFlow = 事件分发 选对了,代码清爽;选错了,bug 不断

最后说一句,选 StateFlow 还是 SharedFlow,没有绝对的对错。但如果你拿不准,就记住这个原则:状态用 StateFlow,事件用 SharedFlow。我在项目里一直这么用,从来没出过问题。

总结:StateFlow 适合需要「当前值」的场景,SharedFlow 适合「一次性事件」场景。配置 SharedFlow 时,replay 控制历史消息数,extraBufferCapacity 控制缓存大小,onBufferOverflow 控制溢出策略。选对了,你的代码会非常清爽;选错了,调试起来够你喝一壶的。


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