StateFlow 与 SharedFlow

85 阅读3分钟

一句话

  • StateFlow=“可观察的单一状态”,始终有值、去重(==)+ 合并(conflate) ,新订阅者必定先拿到“最新值”。
  • SharedFlow=“可配置的事件/流”,可 0/多条回放(replay) 、可配置缓冲/溢出策略,适合一次性事件广播多消费者分发

核心差异对照表

维度StateFlowSharedFlow
是否 HotHot(长期存在)Hot(长期存在)
初始值必须有初始值可无初始值
当前值属性value(可读写)无 value(除非自己加缓存)
去重按 == 去重(新值与旧值相等则不发射)不去重(同值也会再次发)
回放(replay)固定为 1(即“当前值”)可配置 0…N
缓冲/背压合并最新(不挂起)replay + extraBufferCapacity + onBufferOverflow(SUSPEND/DROP_OLDEST/DROP_LATEST)
完成/关闭不会“完成/关闭”也不完成;需自行管理生命周期
典型语义“状态容器/单一真相源(SSOT)”“事件总线/多播流”
常见等价BehaviorSubject(但带去重)Publish/ReplaySubject(取决于 replay)

什么时候用谁?

用 StateFlow:

  • 屏幕/模块的可重放 UI 状态(Compose collectAsStateWithLifecycle())。

  • 设置页、表单、列表筛选等状态单一且需要最新值的场景。

  • 从冷流(Room/DataStore)派生 UI 状态:source.stateIn(...)。

用 SharedFlow:

  • 一次性事件:导航、Toast/Snackbar、Dialog、埋点(replay=0)。

  • 多播事件总线:同一事件给多个订阅者(replay=0/1 視需求)。

  • 需要回放部分历史(如消息流首屏补齐,replay>0)。

  • 需要自定义背压策略(丢最新/丢最老/挂起)。

注意:一次性 UI 事件若担心“切到后台再回来错过”,可以 MutableSharedFlow(replay=1, extraBufferCapacity=0, onBufferOverflow=DROP_OLDEST),并在消费后显式清空或用“事件已消费”标记。


与 Channel 的取舍(易混淆)

  • Channel:点对点(单消费者)队列,会 close,更像“管道”。
  • SharedFlow多播,默认不 close,更像“广播”。
  • UI 事件建议 SharedFlow(或 Channel.receiveAsFlow() 只给一个消费者)。

关键代码

1) UI 状态(StateFlow)

class VM : ViewModel() {
    private val _ui = MutableStateFlow(UiState())
    val ui: StateFlow<UiState> = _ui

    fun toggle() = _ui.update { it.copy(enabled = !it.enabled) }  // 新实例替换很关键
}
@Composable
fun Screen(vm: VM) {
    val state by vm.ui.collectAsStateWithLifecycle()
    // 使用 state 渲染
}

:若 UiState 内含 MutableList 并原地修改,== 可能视作“没变”,不会发射;务必用 新实例 替换(不可变数据/copy)。

2) 一次性事件(SharedFlow)

class VM : ViewModel() {
    private val _events = MutableSharedFlow<UiEvent>(
        replay = 0, extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST
    )
    val events: SharedFlow<UiEvent> = _events

    fun onClick() { _events.tryEmit(UiEvent.ShowToast("Saved")) }
}
@Composable
fun Screen(vm: VM) {
    val lifecycle = LocalLifecycleOwner.current.lifecycle
    LaunchedEffect(Unit) {
        vm.events.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
            .collect { e -> if (e is UiEvent.ShowToast) toast(e.msg) }
    }
}

3) 冷流转热流(stateIn / shareIn)

// 冷流 -> StateFlow(用于 UI 状态)
val ui: StateFlow<UiState> = repo.data
    .map { it.toUi() }
    .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState())

// 冷流 -> SharedFlow(用于多播事件)
val notices: SharedFlow<Notice> = repo.noticeStream
    .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 0)

常见坑位 & 处理

  1. StateFlow 不发射:你做了原地修改(同引用),或新旧值 ==;改为不可变模型 + copy/新列表。

  2. 事件丢失:SharedFlow replay=0 时,订阅还没建立就发了事件 → 新订阅者拿不到;

    • 方案:replay=1 + 消费后清空;或订阅更早(repeatOnLifecycle(STARTED))。
  3. 背压导致挂起:SharedFlow 默认 SUSPEND,高频事件可能让 emit 挂起;

    • 方案:设置 extraBufferCapacity 或 DROP_LATEST/DROP_OLDEST。
  4. Compose 中用 snapshotFlow 监听 StateFlow:无效;直接 collect / collectAsStateWithLifecycle()。

  5. 想“重复发相同值” (刷新):StateFlow 不会重放同值;

    • 方案:引入 version 字段;或改用 MutableSharedFlow(replay=1)。

选型速记(PECS 外一条)

  • “状态”就 StateFlow;“事件”就 SharedFlow。
  • 需要“回放/背压策略/广播配置” → SharedFlow。
  • 需要“当前值/去重/与 Compose 强绑定” → StateFlow。