4. Flow封装进度流:StateFlow与SharedFlow选择、initialValue、replay缓存
将异步任务进度封装为Flow时,面临两个核心选择:StateFlow 还是 SharedFlow?它们的区别决定了流的起始值、重播行为、观察者数量及生命周期。本章从原理出发,结合代码实践,讲解如何根据场景正确选择并配置 initialValue 与 replay 缓存。
4.1 原理:两种有状态Flow的差异
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须设置 initialValue | 可通过 replay 隐式提供初始值 |
| 缓存机制 | 始终保留最新值(等效 replay=1) | 通过 replay 参数指定重播个数(0~N) |
| 观察者接收规则 | 新订阅者立即收到当前最新值 | 新订阅者收到最近 replay 个已发事件(可为空) |
| 多观察者 | 多观察者共享同一个值,无额外缓冲 | 多观察者可独立消费事件流(含粘性) |
| 常用场景 | UI状态快照(如加载中、进度百分比) | 一次性事件(如导航指令、Snackbar) |
选择依据:若需要“当前进度值能被新观察者立即拿到”,用 StateFlow;若只想让新观察者看到最近 N 次进度更新(例如日志流),用 SharedFlow 并设置 replay。
4.2 实现流程:封装任务进度流
以下是使用 StateFlow 封装网络下载进度的典型实现流程:
- 定义 进度状态数据类(如
TaskProgress)。 - 在 ViewModel 中创建
MutableStateFlow,设置initialValue。 - 在异步任务中通过
.value更新进度。 - 对外暴露不可变的
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。