Skip to content

热流 (StateFlow 与 SharedFlow)

源:StateFlow and 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 的销毁与重建。

  1. 销毁阶段:旧 Activity 的协程取消,Flow 的订阅者计数变为 0。
  2. 重建阶段:新 Activity 重新订阅。
  3. 5000ms 策略:如果我们将超时设为 5 秒,那么在屏幕旋转的短暂瞬间(通常 < 1秒),由于计数归零没超过 5 秒,上游的 dataFlow 不会被停止,且新 Activity 订阅后能立即拿到缓存值。
  4. 资源节省:如果用户真的彻底离开了页面,5 秒后上游流会停止,释放内存和 CPU。

核心开发准则

  1. 状态用 StateFlow,事件用 SharedFlow:不要试图在 UI 中通过观察 StateFlow 的变化来弹出 Toast,那会导致旋转屏幕后 Toast 重复弹出。
  2. 永远优先使用 update:养成操作 MutableStateFlow 时调用 updategetAndUpdate 的习惯,规避隐蔽的并发 Bug。
  3. 理解 replay 的代价:过大的 replay 会增加内存占用。对于一次性通知,replay 必须设为 0。
  4. 谨慎使用 MutableSharedFlow.value:事实上它并没有 value 属性。如果你发现自己需要“查看当前发了什么”,那你应该改用 StateFlow
  5. UI 层收集必须感知生命周期:配合 repeatOnLifecycle 收集热流,才能让 WhileSubscribed 策略真正生效。