Skip to content

热流:StateFlow 与原子更新

源:StateFlow API 文档

StateFlow 是现代 Android 开发中替换 LiveData 的首选。它始终持有最新的状态值,且是线程安全的。

原子更新:update

竞态条件

StateFlowvalue 属性是可变的。在并发环境下,直接进行 value = value + 1 是非原子的(Read-Modify-Write),可能导致数据丢失。

正确姿势:使用 .update { ... }

kotlin
// ✅ 安全:内部使用 CAS (Compare-And-Swap) 循环
_uiState.update { currentState ->
    currentState.copy(count = currentState.count + 1)
}

冷流转热流:stateIn

将 Repository 层的冷流转换为 ViewModel 层的热流,以避免每次 UI 订阅都重新触发上游逻辑(如重复网络请求)。

kotlin
val uiState: StateFlow<UiState> = repository.dataFlow
    .map { UiState.Success(it) }
    .stateIn(
        scope = viewModelScope,
        initialValue = UiState.Loading,
        // ⭐️ 黄金参数:5秒超时
        // 允许配置旋转屏幕时 (Stop -> Start) 流保持活跃,不重新加载
        started = SharingStarted.WhileSubscribed(5_000)
    )

事件共享:shareIn

如果你不需要“初始值”(例如:Toast 消息、导航事件、WebSocket 消息),应该使用 SharedFlowshareIn

kotlin
// 将冷流转换为广播热流
val eventFlow = rawSocketFlow
    .shareIn(
        scope = viewModelScope,
        // Eagerly: 立即开始; Lazily: 第一个订阅者出现时开始; WhileSubscribed: 有订阅者时保持
        started = SharingStarted.WhileSubscribed(), 
        replay = 0 // 默认为 0,不重播旧消息
    )

陷阱:SharedFlow 的缓冲区与重播

1. Replay 的副作用 (事件倒灌)

如果你设置 replay > 0,每个新的订阅者都会立即收到最近的 N 个缓存事件。

  • 状态共享 (如用户信息):适合 replay = 1
  • 脉冲事件 (如 Toast):必须 replay = 0,否则旋转屏幕后 Toast 会重弹。

2. tryEmit 失败

MutableSharedFlow 默认的 extraBufferCapacity 为 0,且 onBufferOverflowSUSPEND。 这意味着:

  • emit (挂起函数) 没问题,会挂起直到有订阅者处理。
  • tryEmit (非挂起) 会失败返回 false,如果没有任何订阅者在挂起等待(或者没有缓冲区)。

解决方案:如果你需要使用 tryEmit 发送事件(例如从非协程环境),必须给缓冲区留位置。

kotlin
val _events = MutableSharedFlow<Event>(
    replay = 0,
    extraBufferCapacity = 1, // ⭐️ 至少给 1 个位置缓存
    onBufferOverflow = BufferOverflow.DROP_OLDEST // 避免挂起
)
_events.tryEmit(Event.Show) // ✅ 现在总是成功

StateFlow vs SharedFlow

特性StateFlowSharedFlow
初始值必须有
重播 (Replay)固定为 1 (粘性)可配置 (默认为 0)
防抖自动过滤重复值 (distinctUntilChanged)不过滤
场景UI 状态 (State)一次性事件 (Events)