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

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 只在 STARTEDRESUMED 状态下分发数据,这避免了在后台更新 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 协程中 emitStateFlow,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. 总结

  1. 状态用 StateFlow :替代 MutableLiveData ,具备粘性和去重能力。

  2. 事件用 SharedFlow(replay=0) :替代 SingleLiveEvent,避免旋转重复触发。

  3. 冷流转热流用 stateInWhileSubscribed(5000) 是最佳默认配置。

  4. UI 层用 repeatOnLifecycle:等价于 LiveData 的生命周期感知。

  5. 数据层返回 Flow:保持平台无关,在 ViewModel 层完成热流转换。

> 核心结论:LiveData 适合简单 UI 状态绑定;StateFlow/SharedFlow 是现代 Android 的选择,具备更强的可组合性、可测试性和跨平台能力,迁移成本远低于长期维护 LiveData 的心智负担。


参考资料

相关推荐
JohnnyDeng9420 小时前
【Android】Hilt 依赖注入:原理与最佳实践
android·kotlin·mvvm·hilt
星间都市山脉1 天前
Android STS(Security Test Suite)完整介绍与测试流程
android·java·linux·windows·ubuntu·android studio·androidx
Yeyu1 天前
你真的了解AIDL吗? 附:AIDL 与 Binder 交互全解析
android
dualven_in_csdn1 天前
一键起飞调用示例
android·java·javascript
故渊at1 天前
第十板块:Android 系统稳定性与调试 | 第二十五篇:Watchdog 与 ANR 的系统级监控
android·watchdog·系统稳定性·anr·超时监控
故渊at1 天前
第十板块:Android 系统稳定性与调试 | 第二十六篇:Systrace 与 Perfetto 的系统级性能分析
android·perfetto·性能分析·systrace·系统稳定性
吕工-老船长19981 天前
20260610----S905Y5(Android14)-----连接网络自动更新时间,时间设置为24小时
android
杉氧1 天前
Kotlin 协程深度解析④:架构实战——在 MVVM/MVI 中的进阶应用
android·kotlin
Ab_stupid1 天前
CTF-Android培训笔记
android·笔记