13、LiveData与Flow的选择:StateFlow、SharedFlow在MVVM中的应用
说实话,LiveData和Flow这个话题,几乎每次技术评审都会有人问我。我记得有一次,团队里一个小伙子在代码里混用了三种数据流方案,搞得维护起来特别痛苦。嗯,今天我们就来彻底聊清楚,什么时候该用LiveData,什么时候该换Flow。
为什么会有这个选择难题?
LiveData是Google官方推出的观察者模式组件,它和Activity、Fragment的生命周期绑定得死死的。你想想看,这其实是个双刃剑——好处是不用手动处理取消订阅,坏处是它只能在Android平台上用。
Flow是Kotlin协程家族的一员,它不依赖任何Android组件。说白了,Flow就是一套冷数据流,你每次订阅都会重新执行一遍上游代码。但StateFlow和SharedFlow是它的热数据流变体,行为上更像LiveData。
我在项目中遇到过这样的场景:一个网络请求的结果,既要在Activity里展示,又要传到Service里做后台处理。用LiveData的话,Service里没法直接观察,还得自己写回调。换成SharedFlow,问题就简单多了。
核心结论:如果你只在Android UI层使用,并且需要生命周期感知,LiveData完全够用。一旦涉及跨层通信、复杂转换、或者需要脱离Android环境测试,Flow是更好的选择。
StateFlow vs SharedFlow:别再傻傻分不清
这两个东西长得像,但用法完全不同。我刚开始用的时候也踩过坑,后来总结了一套判断方法。
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须提供 | 可选 |
| 粘性事件 | 默认粘性 | 可配置 |
| 去重 | 自动去重(distinctUntilChanged) | 默认不去重 |
| 典型场景 | UI状态持有 | 一次性事件(如Toast、导航) |
StateFlow:UI状态的最佳载体
我个人习惯用StateFlow来替代LiveData,尤其是在ViewModel里。为什么?因为它天然支持协程,而且去重机制能避免不必要的UI刷新。
// ViewModel层
class MainViewModel : ViewModel() {
private val _uiState = MutableStateFlow(MainUiState())
val uiState: StateFlow<MainUiState> = _uiState.asStateFlow()
fun loadData() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isLoading = true)
try {
val data = repository.fetchData()
_uiState.value = _uiState.value.copy(
isLoading = false,
data = data
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isLoading = false,
error = e.message
)
}
}
}
}
// UI层
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 注意:这里需要lifecycle-runtime-ktx的支持
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
// 更新UI
updateUI(state)
}
}
}
}
}
小技巧:用repeatOnLifecycle来收集Flow,它能保证在生命周期停止时自动取消协程,效果和LiveData一样安全。
SharedFlow:一次性事件的正确打开方式
我曾经在项目里用LiveData发Snackbar,结果每次屏幕旋转都会重复弹出。这就是粘性事件带来的问题。SharedFlow可以完美解决这个痛点。
// ViewModel层
class EventViewModel : ViewModel() {
// replay=0表示新订阅者不会收到之前的事件
private val _events = MutableSharedFlow<UiEvent>(replay = 0)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun performAction() {
viewModelScope.launch {
// 做一些操作
_events.emit(UiEvent.ShowSnackbar("操作成功"))
}
}
}
sealed class UiEvent {
data class ShowSnackbar(val message: String) : UiEvent()
data class NavigateTo(val route: String) : UiEvent()
}
// UI层
class EventActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> {
Snackbar.make(binding.root, event.message, Snackbar.LENGTH_SHORT).show()
}
is UiEvent.NavigateTo -> {
// 导航逻辑
}
}
}
}
}
}
}
注意:SharedFlow的replay参数很关键。设为0就是非粘性,设为1就是粘性。我建议一次性事件都用replay=0,避免重复消费。
MVVM中的实际选型策略
说了这么多理论,我们来画个图,看看在MVVM架构里到底怎么选。
避坑指南:我踩过的那些坑
我曾经在项目里犯过一个低级错误:在ViewModel里用MutableStateFlow直接暴露给外部。结果某个Fragment直接调用了viewModel.uiState.value = ...,绕过了所有业务逻辑。嗯,从那以后我强制要求所有StateFlow都用asStateFlow()转为不可变类型。
还有一个坑是关于SharedFlow的背压。如果你用emit()发送事件,而收集端处理太慢,事件会丢失。我建议用tryEmit()配合BufferOverflow策略来处理这种情况。
// 安全的SharedFlow配置
val events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1, // 额外缓冲区
onBufferOverflow = BufferOverflow.DROP_OLDEST // 丢弃最旧的事件
)
我的建议:新项目直接用Flow全家桶。老项目如果LiveData用得挺好,没必要强行迁移。但新增功能建议用Flow,毕竟协程是Kotlin的未来方向。
总结一下
LiveData和Flow不是非此即彼的关系。你想想看,LiveData在简单场景下确实方便,但Flow的灵活性和跨平台能力是它不具备的。我个人现在的习惯是:
- UI状态 → StateFlow
- 一次性事件 → SharedFlow (replay=0)
- 跨层数据共享 → SharedFlow (replay=1)
- 简单页面 → LiveData(省事)
记住一点:没有银弹。选型的关键是理解你的业务场景,而不是盲目追新。嗯,今天就聊到这里,希望这些经验能帮你少走弯路。