4. Flow封装进度流:StateFlow与SharedFlow选择、initialValue、replay缓存

将异步任务进度封装为Flow时,面临两个核心选择:StateFlow 还是 SharedFlow?它们的区别决定了流的起始值、重播行为、观察者数量及生命周期。本章从原理出发,结合代码实践,讲解如何根据场景正确选择并配置 initialValue 与 replay 缓存。

4.1 原理:两种有状态Flow的差异

特性StateFlowSharedFlow
初始值必须设置 initialValue可通过 replay 隐式提供初始值
缓存机制始终保留最新值(等效 replay=1)通过 replay 参数指定重播个数(0~N)
观察者接收规则新订阅者立即收到当前最新值新订阅者收到最近 replay 个已发事件(可为空)
多观察者多观察者共享同一个值,无额外缓冲多观察者可独立消费事件流(含粘性)
常用场景UI状态快照(如加载中、进度百分比)一次性事件(如导航指令、Snackbar)
选择依据:若需要“当前进度值能被新观察者立即拿到”,用 StateFlow;若只想让新观察者看到最近 N 次进度更新(例如日志流),用 SharedFlow 并设置 replay。

4.2 实现流程:封装任务进度流

以下是使用 StateFlow 封装网络下载进度的典型实现流程:

  1. 定义 进度状态数据类(如 TaskProgress)。
  2. 在 ViewModel 中创建 MutableStateFlow,设置 initialValue。
  3. 在异步任务中通过 .value 更新进度。
  4. 对外暴露不可变的 StateFlow 供 UI 层收集。
// 进度状态模型
data class TaskProgress(
    val percentage: Int = 0,
    val status: String = "准备中"
)

// ViewModel 内
private val _progress = MutableStateFlow(TaskProgress())
val progress: StateFlow<TaskProgress> = _progress.asStateFlow()

fun startDownload() {
    viewModelScope.launch {
        for (i in 1..100) {
            // 模拟耗时任务
            delay(50)
            _progress.value = TaskProgress(percentage = i, status = "下载中 $i%")
        }
        _progress.value = TaskProgress(percentage = 100, status = "完成")
    }
}

若使用 SharedFlow 并希望新订阅者看到最近 3 次进度:

private val _progressShared = MutableSharedFlow<TaskProgress>(
    replay = 3,
    extraBufferCapacity = 10
)
val progressShared: SharedFlow<TaskProgress> = _progressShared.asSharedFlow()

// 发送进度
_progressShared.tryEmit(TaskProgress(25, "解压中"))

4.3 initialValue 的正确使用

StateFlow 的 initialValue 必须反映“未开始”或“默认”状态。常见错误是使用空对象或 null,导致 UI 层额外判断。

警告:若你使用 MutableStateFlow<TaskProgress?>(null),UI 层每次收集时都要处理 null 情况,增加复杂度。建议使用有默认字段的数据类代替 null。

典型初始值配置:

// ✅ 推荐:有意义的初始值
val progress = MutableStateFlow(TaskProgress())

// ❌ 不推荐:null 初始值
val progressNullable = MutableStateFlow<TaskProgress?>(null)

4.4 replay 缓存:控制历史事件重播

SharedFlow 的 replay 参数定义对新订阅者重播的历史事件数量:

  • replay = 0:新订阅者只能收到订阅之后的事件(非粘性)。
  • replay = 1:新订阅者立即收到最近一次事件(类似 LiveData)。
  • replay = N:保留最近 N 个事件,新订阅者按序重播。

对于任务进度流,若需要保存“从开始到当前”的完整递进过程供新订阅者回溯,设置 replay = Int.MAX_VALUE 可能造成内存压力。建议根据业务需要设置适当值(如 5~10)。

4.5 调试:观察流行为

在开发阶段可借助 Flow 的中间操作符检查数据:

progress.onEach { p ->
    Log.d("ProgressFlow", "进度: ${p.percentage}%, 状态: ${p.status}")
}.launchIn(viewModelScope)

// 在 SharedFlow 场景下检查 replay 缓存
progressShared.replayCache.forEach { event ->
    Log.d("ProgressCache", "缓存事件: $event")
}

4.6 部署注意事项

  • StateFlow 默认是“热流”,即使没有观察者也会维护最新值;在 ViewModel 中配合 viewModelScope 使用,避免内存泄漏。
  • SharedFlow 的 extraBufferCapacity 用于在背压时暂存事件,需根据生产消费速率合理设定,避免遗漏进度。
  • 跨进程或跨组件传递进度时,优先使用 SharedFlow(零初始值)并配合序列化,StateFlow 的多观察者共享特性可能不适合。

4.7 常见错误

错误后果修正
用 StateFlow 传递一次性事件(如导航)新订阅者反复收到旧事件改用 SharedFlow(replay=0)
忘记配置 initialValue编译错误必须提供初始值
SharedFlow 未设置 replay 却期望新订阅者拿到最新进度新订阅者看不到历史进度设置 replay = 1
同时使用 emit() 与 tryEmit() 混用可能因协程上下文阻塞丢数据统一使用 tryEmit() 或保证在正确协程中 emit

4.8 性能与稳定性优化

  • 减少不必要重播:若进度更新频率极高(如下载进度每 10ms 刷新),先节流再发射,避免 replay 缓存膨胀。
  • 合理设置缓冲区:SharedFlow 的 extraBufferCapacity 默认 0,为防止丢事件可设为任务周期的预估最大事件数。
  • 使用协程层面的背压处理:结合 Flow.buffer() 或 conflate() 处理慢收集者。
  • 对于 StateFlow:多观察者共享同一对象引用,确保进度数据类是不可变的(使用 data class 的 copy)。

4.9 选择决策流程

核心原则:“状态用 StateFlow,事件用 SharedFlow”。任务进度通常是状态(% 和 status),所以 90% 场景推荐 StateFlow。只有当进度需要被多个独立消费者各自完整接收,或需要重播历史记录时,才选用 SharedFlow。
StateFlow vs SharedFlow 决策流程 任务进度流 需要初始值 作为状态? StateFlow 是 是否需重播? 否 SharedFlow(replay=N) 是 SharedFlow(replay=0) 否 提示:StateFlow 自动提供 replay=1 并强制初始值;SharedFlow 可灵活控制缓存大小,适合一次性事件或日志流。