热流 (StateFlow 与 SharedFlow)
在 Kotlin 协程中,标准的 Flow 是“冷”的,即每一位订阅者都会触发一次独立的生产逻辑。而在 UI 开发和事件总线场景中,我们需要热流 (Hot Flows):它们独立于订阅者存在,在内存中持有状态,并能向多个订阅者广播数据。
热流与冷流的本质区别
| 特性 | 冷流 (Cold Flow) | 热流 (Hot Flow) |
|---|---|---|
| 生命周期 | 订阅时开始,取消时结束 | 独立于订阅者,创建后即运行 |
| 状态持有 | 不持有数据,仅定义生产过程 | 持有数据(内存中存在状态) |
| 订阅行为 | 一对一(Unicast) | 一对多(Multicast / Broadcast) |
| 典型代表 | flow { ... } | StateFlow, SharedFlow |
StateFlow:状态持有者
StateFlow 是专门为“持有最新状态”设计的。它是 LiveData 的现代替代品,但提供了更强的线程安全保障和丰富的流算子支持。
- 始终持值:它必须有一个初始值,且通过
value属性始终可以同步获取最新状态。 - 粘性订阅:新的订阅者在启动时会立即收到当前持有的最新值。
- 自动去重:只有当新值与旧值不相等时(基于
equals),才会向下游发射。
原子更新与并发安全
在并发环境下,不要使用 state.value = state.value.copy()。这种“读取-修改-写入”的操作序列是非原子的。在高频并发更新(如计数器)中,会导致数据丢失。
推荐方案:使用 update 扩展函数
kotlin
// ✅ 安全:内部使用 CAS (Compare-And-Swap) 循环确保原子性
_uiState.update { currentState ->
currentState.copy(count = currentState.count + 1)
}SharedFlow:事件广播器
如果你不需要持有“状态”,而是需要发送“事件”(如显示 Toast、导航跳转),SharedFlow 是最佳选择。
- 无初值要求:它不代表状态,只代表曾经发生过的事件流。
- 高度可配置:允许配置重播数量(replay)和缓冲区大小。
kotlin
val _events = MutableSharedFlow<Event>(
replay = 0, // 新订阅者不会收到旧事件
extraBufferCapacity = 1, // 增加缓冲区防止 tryEmit 失败
onBufferOverflow = BufferOverflow.DROP_OLDEST // 溢出时丢弃旧事件
)kotlin
// ❌ 危险:如果 replay=0 且无缓存,且此时没有任何订阅者挂起接收,
// tryEmit 会直接返回 false 并丢弃事件。
_events.tryEmit(ToastEvent("Hello"))
// ✅ 建议:始终在后台协程使用 emit(value) 挂起发送,
// 或者确保配置了 extraBufferCapacity。
scope.launch { _events.emit(ToastEvent("Hello")) }冷流转热流:stateIn 与 shareIn
这是 Android 开发中将 Repository 层数据提升到 ViewModel 层的标准范式。
kotlin
val uiState: StateFlow<UiState> = repository.dataFlow
.map { it.toUiState() }
.stateIn(
scope = viewModelScope,
initialValue = UiState.Loading,
started = SharingStarted.WhileSubscribed(5000) // ⭐️ 核心策略
)WhileSubscribed(5000) 深度解析
在 Android 中,屏幕旋转会导致 Activity 的销毁与重建。
- 销毁阶段:旧 Activity 的协程取消,Flow 的订阅者计数变为 0。
- 重建阶段:新 Activity 重新订阅。
- 5000ms 策略:如果我们将超时设为 5 秒,那么在屏幕旋转的短暂瞬间(通常 < 1秒),由于计数归零没超过 5 秒,上游的
dataFlow不会被停止,且新 Activity 订阅后能立即拿到缓存值。 - 资源节省:如果用户真的彻底离开了页面,5 秒后上游流会停止,释放内存和 CPU。
核心开发准则
- 状态用 StateFlow,事件用 SharedFlow:不要试图在 UI 中通过观察
StateFlow的变化来弹出 Toast,那会导致旋转屏幕后 Toast 重复弹出。 - 永远优先使用 update:养成操作
MutableStateFlow时调用update或getAndUpdate的习惯,规避隐蔽的并发 Bug。 - 理解 replay 的代价:过大的
replay会增加内存占用。对于一次性通知,replay必须设为 0。 - 谨慎使用 MutableSharedFlow.value:事实上它并没有
value属性。如果你发现自己需要“查看当前发了什么”,那你应该改用StateFlow。 - UI 层收集必须感知生命周期:配合
repeatOnLifecycle收集热流,才能让WhileSubscribed策略真正生效。