一句话
- StateFlow=“可观察的单一状态”,始终有值、去重(==)+ 合并(conflate) ,新订阅者必定先拿到“最新值”。
- SharedFlow=“可配置的事件/流”,可 0/多条回放(replay) 、可配置缓冲/溢出策略,适合一次性事件广播与多消费者分发。
核心差异对照表
| 维度 | StateFlow | SharedFlow |
|---|---|---|
| 是否 Hot | Hot(长期存在) | 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)
常见坑位 & 处理
-
StateFlow 不发射:你做了原地修改(同引用),或新旧值 ==;改为不可变模型 + copy/新列表。
-
事件丢失:SharedFlow replay=0 时,订阅还没建立就发了事件 → 新订阅者拿不到;
- 方案:replay=1 + 消费后清空;或订阅更早(repeatOnLifecycle(STARTED))。
-
背压导致挂起:SharedFlow 默认 SUSPEND,高频事件可能让 emit 挂起;
- 方案:设置 extraBufferCapacity 或 DROP_LATEST/DROP_OLDEST。
-
Compose 中用 snapshotFlow 监听 StateFlow:无效;直接 collect / collectAsStateWithLifecycle()。
-
想“重复发相同值” (刷新):StateFlow 不会重放同值;
- 方案:引入 version 字段;或改用 MutableSharedFlow(replay=1)。
选型速记(PECS 外一条)
- “状态”就 StateFlow;“事件”就 SharedFlow。
- 需要“回放/背压策略/广播配置” → SharedFlow。
- 需要“当前值/去重/与 Compose 强绑定” → StateFlow。