热流:StateFlow 与原子更新
StateFlow 是现代 Android 开发中替换 LiveData 的首选。它始终持有最新的状态值,且是线程安全的。
原子更新:update
竞态条件
StateFlow 的 value 属性是可变的。在并发环境下,直接进行 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 消息),应该使用 SharedFlow 和 shareIn。
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,且 onBufferOverflow 为 SUSPEND。 这意味着:
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
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须有 | 无 |
| 重播 (Replay) | 固定为 1 (粘性) | 可配置 (默认为 0) |
| 防抖 | 自动过滤重复值 (distinctUntilChanged) | 不过滤 |
| 场景 | UI 状态 (State) | 一次性事件 (Events) |