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架构里到底怎么选。

MVVM数据流选型决策图 View层 Activity / Fragment 收集StateFlow更新UI 收集SharedFlow处理事件 ViewModel层 StateFlow → UI状态 SharedFlow → 一次性事件 viewModelScope管理协程 Repository层 返回Flow<T> Room DAO返回Flow Retrofit挂起函数 选型决策表 场景 推荐方案 原因 UI状态(列表、加载中) StateFlow 自动去重,有初始值 一次性事件(Toast、导航) SharedFlow (replay=0) 非粘性,避免重复消费 跨层数据传递 SharedFlow (replay=1) 支持多订阅者,可配置粘性 简单UI绑定(无转换) LiveData 生命周期感知,代码简洁

避坑指南:我踩过的那些坑

我曾经在项目里犯过一个低级错误:在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(省事)

记住一点:没有银弹。选型的关键是理解你的业务场景,而不是盲目追新。嗯,今天就聊到这里,希望这些经验能帮你少走弯路。

公众号:蓝海资料掘金营,微信deep3321