Flow vs LiveData:选型指南与迁移实践

> 一句话收益:彻底搞清 Flow 和 LiveData 的本质差异,避免在错误场景下选错工具,掌握从 LiveData 平滑迁移到 StateFlow/SharedFlow 的完整路径。
> 适用版本:Android 5.0+ / Kotlin 1.6+ / Coroutines 1.6+ / Lifecycle 2.5+
> 阅读时长:约 18 分钟
1. 从一个真实 Bug 切入
你一定见过这样的代码:
// ViewModel
val userState = MutableLiveData
()
// Fragment
viewModel.userState.observe(viewLifecycleOwner) { user ->
updateUI(user)
}
看起来无懈可击,但在实际项目中出现了一个诡异 Bug:用户从登录页跳转到首页后,首页竟然显示了上一次登录用户的数据,停留约 200ms 后才更新到当前用户。
问题根源:LiveData 的粘性事件(Sticky Event)机制。当新的 Observer 订阅时,LiveData 会立即将当前持有的最后一个值回放给观察者。在导航场景下,新 Fragment 会收到前一个值,产生短暂的"闪屏"。
而使用 SharedFlow(replay=0) 则可以彻底规避这个问题------这正是本文要探讨的选型关键。
2. LiveData 与 Flow 全景
2.1 核心设计哲学对比
LiveData Flow/StateFlow/SharedFlow
───────────────────────── ─────────────────────────────
Android 专属 Kotlin 多平台,协议中立
观察者模式(推送) 冷流/热流,背压支持
主线程安全(自动切回主线程) 需显式 flowOn/collect
感知生命周期(自动 inactive) 需 repeatOnLifecycle 包裹
粘性事件(默认回放最后值) 可精确控制 replay 行为
无背压支持 完整背压语义(suspend collect)
2.2 Flow 家族一览
kotlin.coroutines.Flow(冷流)
│
├── StateFlow(热流·状态) 替代 LiveData
的首选
│ replay = 1(始终持有最新值)
│ 相同值不触发更新(distinctUntilChanged 语义)
│
└── SharedFlow(热流·事件) 替代 SingleLiveEvent 的首选
replay = N(可配置历史缓存)
相同值也触发更新
2.3 冷流 vs 热流
| 特征 | 冷流(Flow)| 热流(StateFlow/SharedFlow)|
|---|---|---|
| 开始执行 | 有 collector 时才执行 | 立即执行,独立于 collector |
| 数据共享 | 每个 collector 独立执行 | 所有 collector 共享同一流 |
| 典型场景 | 数据库查询、网络请求 | UI 状态、事件总线 |
3. 核心原理深度解析
3.1 LiveData 的生命周期感知原理
Activity/Fragment
│ getLifecycle()
▼
LifecycleOwner ──► LifecycleRegistry
│ addObserver()
▼
LifecycleBoundObserver (LiveData 内部类)
│
├── onStateChanged(STARTED/RESUMED) → 激活,通知最新值
└── onStateChanged(STOPPED/DESTROYED) → 停止观察/移除
关键源码路径:androidx.lifecycle.LiveData#observe() → LifecycleBoundObserver
LiveData 只在 STARTED 或 RESUMED 状态下分发数据,这避免了在后台更新 UI 导致的崩溃。但这也意味着任何在 STOPPED 状态期间产生的更新都只会保留最后一个,当 Observer 重新激活时一次性收到。
3.2 StateFlow 与 SharedFlow 的内部机制
StateFlow 本质是 SharedFlow(replay=1, onBufferOverflow=DROP_OLDEST) 的特化版本,内部用 _state: AtomicRef 保存当前值,并在每次 emit 时通过 equalsCheck 过滤重复值。
MutableStateFlow
▼
StateFlowImpl
│ _state: AtomicRef
│
├── emit(value) → 原子更新 → 唤醒等待中的 collector
└── collect { } → 注册 Slot → suspend 等待新值
SharedFlow 使用环形缓冲区(circular buffer)存储 replay 历史, emit 是 suspend 函数(当 buffer 满时会挂起); tryEmit 是非挂起版本(buffer 满时返回 false)。
3.3 repeatOnLifecycle 为什么是必须的
// ❌ 危险写法:在 lifecycleScope 直接 collect
lifecycleScope.launch {
viewModel.uiState.collect { ... }
// App 进后台后 Activity 进入 STOPPED,但这个协程仍在跑
// 浪费 CPU + 可能触发 UI 更新崩溃
}
// ✅ 正确写法:用 repeatOnLifecycle
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { ... }
// 进入 STARTED 时启动新协程,进入 STOPPED 时取消协程
}
}
repeatOnLifecycle 内部监听 LifecycleEventObserver,在 ON_START 时启动一个子协程并调用 block(),在 ON_STOP 时取消这个子协程。这与 LiveData 的生命周期感知是等价的, 是 Flow 安全收集 UI 状态的唯一推荐方式 。
4. 代码示例
4.1 正确的 ViewModel 状态建模
// 定义 UI 状态密封类
sealed class UserUiState {
object Loading : UserUiState()
data class Success(val user: User) : UserUiState()
data class Error(val message: String) : UserUiState()
}
// ViewModel 实现
class UserViewModel(
private val userRepo: UserRepository
) : ViewModel() {
// 私有可变 StateFlow,外部只暴露只读版本
private val _uiState = MutableStateFlow
(UserUiState.Loading)
val uiState: StateFlow
= _uiState.asStateFlow()
// 一次性事件用 SharedFlow(replay=0 确保不粘性)
private val _events = MutableSharedFlow
()
val events: SharedFlow
= _events.asSharedFlow()
fun loadUser(userId: String) {
viewModelScope.launch {
_uiState.value = UserUiState.Loading
try {
val user = userRepo.getUser(userId) // 挂起函数
_uiState.value = UserUiState.Success(user)
} catch (e: Exception) {
_uiState.value = UserUiState.Error(e.message ?: "Unknown error")
_events.emit(UiEvent.ShowSnackbar(e.message ?: "Error"))
}
}
}
}
4.2 Fragment 中的安全收集
class UserFragment : Fragment(R.layout.fragment_user) {
private val viewModel: UserViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.uiState.collect { state ->
when (state) {
is UserUiState.Loading -> showLoading()
is UserUiState.Success -> showUser(state.user)
is UserUiState.Error -> showError(state.message)
}
}
}
launch {
viewModel.events.collect { event ->
when (event) {
is UiEvent.ShowSnackbar -> showSnackbar(event.message)
}
}
}
}
}
}
}
4.3 错误写法 → 问题 → 正确写法
场景:ViewModel 中将 Flow 转换暴露给 UI
// ❌ 错误写法
class BadViewModel : ViewModel() {
// 每次 collect 都会重新执行数据库查询!冷流!
val users: Flow
> = userDao.getAllUsers()
}
// 问题:冷流每个 collector 独立触发上游;无初始值;旋转屏幕重新查询
// ✅ 正确写法
class GoodViewModel : ViewModel() {
val users: StateFlow
> = userDao.getAllUsers()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000), // 5秒无订阅停止上游
initialValue = emptyList()
)
}
5. 最佳实践
5.1 UI 状态用 StateFlow,一次性事件用 SharedFlow
做法 : StateFlow 持有可观察的 UI 状态; SharedFlow(replay=0) 发送导航、Snackbar 等一次性事件。 原因 :UI 状态需要粘性(新 Observer 立即获取当前状态);一次性事件绝不能粘性(否则旋转屏幕后 Snackbar 重新弹出)。 对比 :不区分会导致旋转屏幕时重复触发 Toast/Snackbar/导航,体验极差。
5.2 始终用 stateIn 将冷流转热流后暴露给 UI
做法 : flow.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue)。 原因 :避免多个 collector 触发多次上游执行;提供初始值减少 UI 空白时间。 对比 :直接暴露冷 Flow,每次 Activity 重建都会重新执行数据库查询,浪费资源。
5.3 在 UI 层只使用 repeatOnLifecycle 收集
做法 :所有 UI 层的 collect 都包裹在 repeatOnLifecycle(Lifecycle.State.STARTED) 中。 原因 :与 LiveData 的生命周期感知行为完全对齐,防止后台 UI 更新崩溃。 对比 :直接在 lifecycleScope.launch 中 collect,App 进后台时协程仍运行,部分机型触发崩溃。
5.4 数据层返回 Flow,不返回 LiveData
做法 :Repository 和 DAO 返回 Flow ,在 ViewModel 中转换。 原因 :数据层不应感知 Android 生命周期,Flow 是纯 Kotlin,单元测试无 Android 依赖。 对比 :数据层返回 LiveData,单元测试需要 InstantTaskExecutorRule,增加测试复杂度。
6. 常见坑点
坑 1:旋转屏幕导致 SharedFlow 事件丢失
现象 :发出导航事件时,如果恰好在旋转中,新 Fragment 启动后没有触发导航。 原因 : SharedFlow(replay=0) 不缓存历史值,旋转期间没有活跃 collector,事件被丢弃。 复现 : _events.emit(NavigateToDetail) → 立刻旋转屏幕 → 新 Fragment 未导航。 解决 :将导航目标编码进 StateFlow 的 UI 状态中,消费后置 null。
data class UiState(
val navigateTo: String? = null
)
// 消费后:_uiState.update { it.copy(navigateTo = null) }
坑 2:在 Fragment.onCreateView 中启动 collect 导致重复订阅
现象 :Fragment 进出 Back Stack 后,同一状态被多次处理(如多次弹出 Toast)。 原因 : onCreateView 在 Fragment 重新可见时会再次调用,导致在 lifecycleScope 中累积多个 collect 协程。 复现 :A → B → 返回 A,A 的 UI 更新被触发两次。 解决 :在 onViewCreated 中使用 viewLifecycleOwner.lifecycleScope + repeatOnLifecycle。
坑 3:StateFlow 值相同时不触发更新
现象 :调用 _state.value = sameObject 后 UI 没有刷新。 原因 : StateFlow 内置结构相等检查,相同值不会触发下游。 解决 :确保每次更新创建新对象( data class copy 或新列表实例)。
坑 4:非主线程 emit 导致 UI 崩溃
现象 :在 IO 协程中 emit 到 StateFlow,collect 中直接操作 View 导致崩溃。 原因 :collect 的调度器跟随 emit 所在调度器,非主线程更新 View 触发 CalledFromWrongThreadException。 解决 : viewModelScope.launch(Dispatchers.Main) 切换线程,或确保 collect 始终在 repeatOnLifecycle 包裹下(默认主线程)。
坑 5:SharingStarted.Eagerly 导致测试竞争
现象 :单元测试中, stateIn(Eagerly) 的 Flow 在测试协程结束前就开始收集,导致用例顺序影响结果。 原因 : Eagerly 立即启动上游 Flow,与测试调度器产生竞争。 解决 :将 SharingStarted 作为构造参数注入,测试时传入 WhileSubscribed(0) 或 Lazily。
7. 总结
-
状态用 StateFlow :替代
MutableLiveData,具备粘性和去重能力。 -
事件用 SharedFlow(replay=0) :替代
SingleLiveEvent,避免旋转重复触发。 -
冷流转热流用 stateIn :
WhileSubscribed(5000)是最佳默认配置。 -
UI 层用 repeatOnLifecycle:等价于 LiveData 的生命周期感知。
-
数据层返回 Flow:保持平台无关,在 ViewModel 层完成热流转换。
> 核心结论:LiveData 适合简单 UI 状态绑定;StateFlow/SharedFlow 是现代 Android 的选择,具备更强的可组合性、可测试性和跨平台能力,迁移成本远低于长期维护 LiveData 的心智负担。
参考资料
-
AOSP 源码:
frameworks/support/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/LiveData.java -
AOSP 源码:
kotlinx.coroutines/kotlinx-coroutines-core/common/src/flow/StateFlow.kt