6、StateFlow与SharedFlow:StateFlow的使用场景、SharedFlow的使用场景、与LiveData的迁移
说到 Flow 家族里的两个重要成员——StateFlow 和 SharedFlow,我其实在项目里跟它们打过不少交道。刚开始接触时也觉得有点绕,但用顺手之后,你会发现它们比 LiveData 灵活太多了。
说白了,StateFlow 和 SharedFlow 都是 Kotlin 协程体系下的热流(Hot Flow)。什么叫热流?就是不管有没有订阅者,数据都在那里,随时可以取。跟冷流(Cold Flow)不一样,冷流是每次订阅才重新执行生产逻辑。
嗯,我们先从 StateFlow 说起。
StateFlow:状态持有者
StateFlow 的核心特点就一个——它始终持有最新的状态值。你任何时候订阅它,都能立刻拿到当前值。这一点跟 LiveData 非常像。
我个人习惯把 StateFlow 用在 ViewModel 里,作为 UI 状态的容器。举个例子:
class MainViewModel : ViewModel() {
// 定义一个 StateFlow,初始值为 Loading
private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = repository.fetchData()
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
}
你看,StateFlow 通过 .value 直接读写,非常直观。而且它天然支持协程的 collect 操作,在 Fragment 或 Activity 里收集起来也很方便。
我在项目中遇到过一个问题:多个 Fragment 共享同一个 ViewModel 时,StateFlow 的粘性导致每次 Fragment 重新创建都会重新触发一次数据加载。后来我用了 replay 参数为 0 的 SharedFlow 才解决。
SharedFlow:事件总线
SharedFlow 比 StateFlow 更通用。它不强制持有最新值,你可以配置重放(replay)数量、缓冲区大小等。说白了,StateFlow 是 SharedFlow 的一个特例——replay = 1 且没有缓冲区溢出策略。
SharedFlow 最适合的场景是——一次性事件。比如导航、Snackbar 提示、弹窗等。这些事件不应该被重放,否则用户会看到重复的提示。
class EventViewModel : ViewModel() {
// 定义一个 SharedFlow,replay = 0,不重放
private val _events = MutableSharedFlow<UiEvent>(replay = 0)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun triggerEvent() {
viewModelScope.launch {
_events.emit(UiEvent.ShowSnackbar("操作成功"))
}
}
}
sealed class UiEvent {
data class ShowSnackbar(val message: String) : UiEvent()
data class NavigateTo(val route: String) : UiEvent()
}
这里要注意,SharedFlow 的 emit 是挂起函数,如果收集端处理太慢,可能会阻塞发送端。我建议用 tryEmit 做非阻塞发送,或者配置合适的缓冲区策略。
与 LiveData 的迁移
很多团队从 LiveData 迁移到 StateFlow,原因其实很简单——LiveData 不支持协程,而且只能在 Android 主线程上观察。StateFlow 配合 flowWithLifecycle 可以做到同样的事情,还更灵活。
迁移步骤其实不复杂:
- ViewModel 层:把
MutableLiveData换成MutableStateFlow,暴露为StateFlow - 数据层:把
LiveData的转换操作(如map、switchMap)换成 Flow 的map、flatMapLatest - UI 层:把
observe换成collect,配合repeatOnLifecycle或flowWithLifecycle
举个例子,这是 LiveData 的写法:
// LiveData 写法
val userLiveData: LiveData<User> = repository.getUserLiveData()
viewModel.userLiveData.observe(viewLifecycleOwner) { user ->
// 更新 UI
}
迁移成 StateFlow 后:
// StateFlow 写法
val userStateFlow: StateFlow<User> = repository.getUserFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = User()
)
// 在 Fragment 中收集
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userStateFlow.collect { user ->
// 更新 UI
}
}
}
你想想看,StateFlow 的优势在哪里?
- 协程原生:可以跟其他 Flow 操作符无缝组合
- 线程安全:不需要
postValue这种 hack - 测试友好:可以用
TestCoroutineDispatcher控制时间 - 生命周期感知:配合
repeatOnLifecycle自动管理
不过也要注意,StateFlow 不是 LiveData 的完全替代品。LiveData 在 DataBinding 里用起来更顺手,如果你项目重度依赖 DataBinding,暂时保留 LiveData 也没问题。
最后,我整理了一张对比图,帮你快速理解三者的区别:
总结一下我的经验:
- UI 状态(如加载中、数据、错误):用 StateFlow,粘性行为正好符合需求
- 一次性事件(如导航、Snackbar):用 SharedFlow,replay = 0
- 旧项目迁移:从 LiveData 逐步迁移到 StateFlow,不要一次性全改
嗯,StateFlow 和 SharedFlow 其实没那么神秘。你只要记住——StateFlow 是带状态的,SharedFlow 是带事件的。用对场景,代码会清爽很多。