【Android】Flow vs LiveData:选型指南与迁移实践 Flow vs LiveData选型指南与迁移实践一句话收益彻底搞清 Flow 与 LiveData 的本质区别掌握何时选用、如何迁移杜绝随机选一个的尴尬。适用版本Android API 21Kotlin 1.9Lifecycle 2.7Coroutines 1.8阅读时长约 18 分钟---1. 从一个真实 Bug 切入你的项目里有段代码大致是这样// ViewModelval searchResults MutableLiveData ()fun search(query: String) {viewModelScope.launch {repository.search(query).collect { results -searchResults.value results // 后台线程调用 .value 崩溃}}}运行时偶发CalledFromWrongThreadExceptionCannot invoke setValue on a background thread。根本原因Flow.collect默认在协程调度器可能是IO运行LiveData.value只能在主线程赋值。开发者将两套数据流体系混用导致线程语义冲突。这个 Bug 揭示了一个深层问题LiveData 和 Flow 分别为不同场景而生混用时必须清楚各自的线程契约。---2. LiveData 与 Flow 全景2.1 两者的核心定位LiveData Flow (Kotlin Coroutines)────────────────────────────── ──────────────────────────────生命周期感知数据容器 冷/热数据流抽象专为 UI 层设计 通用响应式流任意层可用主线程交付 调度器灵活仅支持最新值无背压 支持背压buffer/conflate观察者自动解绑 需手动管理 collect 生命周期Java 友好 Kotlin 原生2.2 冷流 vs 热流Cold Flow (默认 flow {})每次 collect → 重新执行 blockCollector 1 ──collect──→ [producer runs] → emit(1) → emit(2)Collector 2 ──collect──→ [producer runs again] → emit(1) → emit(2)Hot Flow (SharedFlow / StateFlow)独立生产多消费者共享Producer ──emit──→ [SharedFlow buffer]├──→ Collector 1└──→ Collector 2LiveData本质上是热的setValue 随时可调但没有 buffer只保留最新值类似 StateFlow(replay1) 生命周期感知2.3 StateFlow 与 LiveData 的相似性StateFlow是最接近 LiveData 的 Flow 类型| 特性 | LiveData | StateFlow ||------|----------|-----------|| 保存最新值 | ✅ | ✅ || 初始值 | 可为 null | 必须有初始值 || 生命周期感知 | ✅ 原生 | ❌ 需repeatOnLifecycle|| 线程安全 | 主线程 setValue | 任意线程 value/emit || 去重 | ❌ | ✅结构相等判断 || 测试 | 需InstantTaskExecutorRule| 原生协程测试 |---3. 核心原理3.1 LiveData 的生命周期感知原理LiveData通过LifecycleOwner注册LifecycleObserver在DESTROYED时自动移除观察者LiveData.ObserverWrapper.shouldBeActiveActivity/Fragment (LifecycleOwner)│└─ observe(owner, observer)│└─ owner.lifecycle.addObserver(LifecycleBoundObserver)│└─ onStateChanged(DESTROYED) → removeObserver()AOSP 路径frameworks/support/lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/LiveData.java关键方法LiveData#observe、LifecycleBoundObserver#onStateChanged3.2 Flow 的 collect 与协程取消flow {}是冷流collect触发 block 执行。协程取消时cancel()或作用域结束collect内部通过CancellationException传播取消上游emit会检查取消状态ensureActive()viewModelScope.launch { // 作用域flow { emit(1); emit(2) } // 冷流 block.collect { value - // 触发执行// 协程取消 → CancellationException → block 停止}} // ViewModel 销毁 → viewModelScope 取消 → collect 停止AOSP 路径kotlinx-coroutines-core/common/src/flow/Flow.kt关键方法AbstractFlow#collect、FlowCollector#emit3.3 repeatOnLifecycle补齐 Flow 的生命周期感知repeatOnLifecycle(Lifecycle.State.STARTED)会在生命周期进入 STARTED 时启动新协程离开 STARTED 时取消从而实现和 LiveData 等价的生命周期感知ON_START → launch { stateFlow.collect { } }ON_STOP → cancel() // 协程取消停止收集ON_START → launch { stateFlow.collect { } } // 重新开始ON_DESTROY → 外层协程取消---4. 代码示例4.1 正确写法ViewModel 使用 StateFlowclass SearchViewModel(private val repo: SearchRepository) : ViewModel() {private val _query MutableStateFlow()// 对 UI 只暴露不可变 StateFlowval searchResults: StateFlow _query.debounce(300) // 防抖300ms 内只处理最后一次.filter { it.length 2 } // 过滤短查询.flatMapLatest { query - // 取消旧请求执行新请求if (query.isEmpty()) flowOf(emptyList())else repo.search(query) // 返回 Flow }.stateIn(scope viewModelScope,started SharingStarted.WhileSubscribed(5_000), // UI 消失 5s 后停止initialValue emptyList())fun onQueryChange(query: String) {_query.value query}}// Fragment 中收集class SearchFragment : Fragment() {private val vm: SearchViewModel by viewModels()override fun onViewCreated(view: View, savedInstanceState: Bundle?) {// 使用 repeatOnLifecycle 保证前后台安全viewLifecycleOwner.lifecycleScope.launch {viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {vm.searchResults.collect { items -adapter.submitList(items)}}}}}4.2 错误写法 → 问题 → 正确写法错误写法 1在 IO 线程给 LiveData.value 赋值// ❌ 错误在协程 IO 调度器中调用 .valuefun loadData() {viewModelScope.launch(Dispatchers.IO) {val result repo.fetchData()_liveData.value result // CalledFromWrongThreadException!}}问题LiveData.setValue()只能在主线程调用。// ✅ 正确使用 postValue 或切换到主线程fun loadData() {viewModelScope.launch(Dispatchers.IO) {val result repo.fetchData()_liveData.postValue(result) // 线程安全}}// 或更好直接在主线程协程中操作fun loadData() {viewModelScope.launch { // 默认 Mainval result withContext(Dispatchers.IO) { repo.fetchData() }_liveData.value result}}错误写法 2在 Fragment onStart 中直接 launch collect不用 repeatOnLifecycle// ❌ 错误onStart 里 launch会在 onStop 后继续收集override fun onStart() {super.onStart()lifecycleScope.launch {vm.uiState.collect { render(it) }// onStop 后 collect 还在运行浪费资源后台更新 UI 可能崩溃}}问题协程在lifecycleScope内Fragment 销毁时才取消但 onStop→onStart 之间仍在收集。// ✅ 正确repeatOnLifecycle 在 STARTED 时收集STOPPED 时暂停override fun onViewCreated(view: View, savedInstanceState: Bundle?) {viewLifecycleOwner.lifecycleScope.launch {viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {vm.uiState.collect { render(it) }}}}错误写法 3StateFlow 去重导致 UI 不刷新// ❌ 错误修改同一个 list 引用val list _state.value.itemslist.add(newItem)_state.value _state.value.copy(items list) // equals 可能为 true不 emit// ✅ 正确创建新 list_state.value _state.value.copy(items _state.value.items newItem // 创建新 List)---5. 最佳实践5.1 新项目Repository 层用 FlowViewModel 层用 StateFlow/SharedFlowUI 层用 collectWithLifecycle做法Repository 返回FlowViewModel 用stateIn转为StateFlowFragment/Activity 用repeatOnLifecycle或collectWithLifecycle收集。原因Flow 在 Repository 层保持灵活可背压、可组合操作符StateFlow 在 ViewModel 给 UI 提供稳定的单一状态来源repeatOnLifecycle保证后台不更新 UI。对比若不这样做直接在 Repository 用 LiveData导致 LiveData 渗透到数据层污染非 Android 模块无法在纯 JVM 单元测试中使用。5.2 一次性事件用 SharedFlow(replay0)不要用 LiveData做法导航跳转、Toast 显示等一次性事件使用MutableSharedFlow (replay0)UI 用repeatOnLifecycle(STARTED)收集。原因LiveData 粘性特性新观察者收到旧值会导致一次性事件在屏幕旋转后重复触发。SharedFlow(replay0)没有缓存不会粘性重放。对比用 LiveData SingleLiveEvent 模式虽能解决但 SingleLiveEvent 是社区方案多观察者时仍有 bugSharedFlow 是官方推荐方案。5.3 使用 SharingStarted.WhileSubscribed(5000) 代替 Eagerly做法stateIn的started参数使用SharingStarted.WhileSubscribed(5_000)5 秒超时。原因UI 进入后台后 5 秒内无订阅者则停止上游 Flow避免后台持续请求网络/数据库5 秒宽限期应对屏幕旋转旋转时 Activity 重建约 1-2 秒。对比Eagerly在 ViewModel 创建时立即启动并永不停止后台仍消耗资源Lazily第一次有订阅者后永不停止无法节省资源。5.4 测试时用 Turbine 库替代手动 collect做法引入app.cash.turbine:turbine用flow.test { assertThat(awaitItem()).isEqualTo(expected) }测试 Flow。原因手动launch collect delay测试 Flow 容易出现竞争条件Turbine 提供确定性的awaitItem()不需要 delay。对比不用 Turbine 则需要大量样板代码且测试结果可能因调度时序不同而 flaky。5.5 避免在 ViewModel 中持有 LiveData observer做法ViewModel 中如需观察另一个 LiveData转用 FlowliveData.asFlow()并在viewModelScope中 collect而非observeForever。原因observeForever不自动解绑需手动removeObserver容易遗漏造成内存泄漏Flow 协程作用域会随 ViewModel 销毁自动取消。---6. 常见坑点坑点 1StateFlow 去重导致 UI 无法刷新现象明明调用了_state.value newStateUI 没有刷新。原因StateFlow 使用结构相等equals去重若新值与旧值为 true不会 emit。复现data class 包含 List修改 List 内元素但不创建新对象时equals仍为 true。解决确保每次 emit 创建新对象copy()或对包含可变集合的 data class 重写equals。坑点 2launchIn 遗忘 lifecycleScope在后台继续收集现象App 进入后台后 CPU 仍有持续活动内存不释放。原因用flow.launchIn(viewModelScope)代替collect但 ViewModel 的 scope 不感知生命周期。复现在 Fragment 中写vm.flow.launchIn(lifecycleScope)但不用repeatOnLifecycle。解决UI 层收集必须配合repeatOnLifecycle。// ✅ 正确lifecycleScope.launch {repeatOnLifecycle(Lifecycle.State.STARTED) {vm.uiFlow.onEach { render(it) }.launchIn(this)}}坑点 3LiveData.observe 在 ViewPager2 Fragment 中多次触发现象ViewPager2 切换 tab 后旧 Fragment 的观察者仍在触发出现重复处理。原因observe(viewLifecycleOwner, observer)注册了多个 observerLiveData 粘性特性使旧值立即触发所有 observer。复现快速切换 ViewPager2 tab打印 observer 回调次数。解决迁移到StateFlowrepeatOnLifecycle若保留 LiveData确保只在onViewCreated中注册且用viewLifecycleOwner。坑点 4Room 返回 Flow 在主线程 collect 导致 ANR现象Room query 返回Flow 在主线程 collect 时 ANR。原因配置了allowMainThreadQueries()时大量数据查询阻塞主线程。复现allowMainThreadQueries() Room Flow 大表。解决移除allowMainThreadQueries()Room Flow 自动在IO线程执行。---7. 总结1.LiveData 专为 UI 层设计生命周期感知开箱即用适合维护老代码或简单 ViewModel → UI 数据绑定。2.StateFlow 是 LiveData 的现代替代线程安全、去重、原生协程支持配合repeatOnLifecycle可完全替代 LiveData。3.Repository / 数据层统一用 Flow避免 LiveData 渗透到非 Android 模块提升可测试性。4.一次性事件用 SharedFlow(replay0)彻底解决 LiveData 粘性导致的事件重复问题。5.SharingStarted.WhileSubscribed(5000)是最佳 stateIn 策略平衡资源消耗与屏幕旋转体验。核心结论新代码首选 StateFlow Flow repeatOnLifecycleLiveData 留给遗留代码维护。---参考资料- 官方文档从 LiveData 迁移到 Kotlin Flow- 官方文档StateFlow 和 SharedFlow- 官方文档repeatOnLifecycle- AOSP: LiveData.java- AOSP: StateFlow.kt- Turbine 测试库