news 2026/6/10 14:06:55

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Android】Flow vs LiveData:选型指南与迁移实践

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

>一句话收益:彻底搞清 Flow 与 LiveData 的本质区别,掌握何时选用、如何迁移,杜绝"随机选一个"的尴尬。

>适用版本:Android API 21+,Kotlin 1.9+,Lifecycle 2.7+,Coroutines 1.8+

>阅读时长:约 18 分钟

---

1. 从一个真实 Bug 切入

你的项目里有段代码大致是这样:

// ViewModel

val 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 → 重新执行 block

Collector 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 2

LiveData

本质上是热的(setValue 随时可调)

但没有 buffer,只保留最新值

类似 StateFlow(replay=1) + 生命周期感知

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.shouldBeActive):
Activity/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#observeLifecycleBoundObserver#onStateChanged

3.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#collectFlowCollector#emit

3.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 使用 StateFlow

class SearchViewModel(private val repo: SearchRepository) : ViewModel() {

private val _query = MutableStateFlow("")

// 对 UI 只暴露不可变 StateFlow

val 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 调度器中调用 .value

fun 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 { // 默认 Main

val 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) }

}

}

}

错误写法 3:StateFlow 去重导致 UI 不刷新
// ❌ 错误:修改同一个 list 引用

val list = _state.value.items

list.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 层用 Flow,ViewModel 层用 StateFlow/SharedFlow,UI 层用 collectWithLifecycle

做法:Repository 返回Flow,ViewModel 用stateIn转为StateFlow,Fragment/Activity 用repeatOnLifecyclecollectWithLifecycle收集。原因:Flow 在 Repository 层保持灵活(可背压、可组合操作符);StateFlow 在 ViewModel 给 UI 提供稳定的单一状态来源;repeatOnLifecycle保证后台不更新 UI。对比:若不这样做,直接在 Repository 用 LiveData,导致 LiveData 渗透到数据层,污染非 Android 模块,无法在纯 JVM 单元测试中使用。

5.2 一次性事件用 SharedFlow(replay=0),不要用 LiveData

做法:导航跳转、Toast 显示等一次性事件,使用MutableSharedFlow (replay=0),UI 用repeatOnLifecycle(STARTED)收集。原因:LiveData 粘性特性(新观察者收到旧值)会导致一次性事件在屏幕旋转后重复触发。SharedFlow(replay=0)没有缓存,不会粘性重放。对比:用 LiveData + SingleLiveEvent 模式虽能解决,但 SingleLiveEvent 是社区方案,多观察者时仍有 bug;SharedFlow 是官方推荐方案。

5.3 使用 SharingStarted.WhileSubscribed(5000) 代替 Eagerly

做法stateInstarted参数使用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,转用 Flow(liveData.asFlow())并在viewModelScope中 collect,而非observeForever原因observeForever不自动解绑,需手动removeObserver,容易遗漏造成内存泄漏;Flow + 协程作用域会随 ViewModel 销毁自动取消。

---

6. 常见坑点

坑点 1:StateFlow 去重导致 UI 无法刷新

现象:明明调用了_state.value = newState,UI 没有刷新。原因:StateFlow 使用结构相等(equals)去重,若新值与旧值==为 true,不会 emit。复现:data class 包含 List,修改 List 内元素但不创建新对象时equals仍为 true。解决:确保每次 emit 创建新对象(copy()),或对包含可变集合的 data class 重写equals

坑点 2:launchIn 遗忘 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)

}

}

坑点 3:LiveData.observe 在 ViewPager2 + Fragment 中多次触发

现象:ViewPager2 切换 tab 后,旧 Fragment 的观察者仍在触发,出现重复处理。原因observe(viewLifecycleOwner, observer)注册了多个 observer,LiveData 粘性特性使旧值立即触发所有 observer。复现:快速切换 ViewPager2 tab,打印 observer 回调次数。解决:迁移到StateFlow+repeatOnLifecycle;若保留 LiveData,确保只在onViewCreated中注册,且用viewLifecycleOwner

坑点 4:Room 返回 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(replay=0),彻底解决 LiveData 粘性导致的事件重复问题。

5.SharingStarted.WhileSubscribed(5000)是最佳 stateIn 策略,平衡资源消耗与屏幕旋转体验。

>核心结论:新代码首选 StateFlow + Flow + repeatOnLifecycle;LiveData 留给遗留代码维护。

---

参考资料

- 官方文档:从 LiveData 迁移到 Kotlin Flow

- 官方文档:StateFlow 和 SharedFlow

- 官方文档:repeatOnLifecycle

- AOSP: LiveData.java

- AOSP: StateFlow.kt

- Turbine 测试库

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/10 14:06:47

【鸿蒙】ArkUI 列表性能优化:LazyForEach 与组件复用深度解析

ArkUI 列表性能优化:LazyForEach 与组件复用深度解析 掌握本文后,你将能诊断并修复鸿蒙应用中最常见的列表卡顿问题,让万级数据量的 List 组件在中低端机型上也能丝滑滚动。 适用版本:HarmonyOS NEXT / API 12 阅读时长&#xf…

作者头像 李华
网站建设 2026/6/10 14:04:00

深度学习图神经网络 (GNN) —— 处理图结构数据(六十九)

1. 定位导航 🎉 处理"关系型"数据的利器! 前面的 CNN 处理网格(图像),RNN/Transformer 处理序列(文本)。但现实世界很多数据是图结构: 领域 图 社交网络 用户=节点,好友=边 分子 原子=节点,化学键=边 知识图谱 实体=节点,关系=边 交通网络 路口=节点,…

作者头像 李华
网站建设 2026/6/10 14:03:01

git 拉取项目(mac)

在github商添加SSH Key 的步骤:1st:首先检查自己之前有没有生成过密钥ls -al ~/.ssh2nd:如果能进入到 .ssh 文件目录下,则证明之前生成过 .ssh 密钥,可以直接使用里面的密钥。 如果不能进入到 .ssh 文件目录下&#xf…

作者头像 李华
网站建设 2026/6/10 13:56:35

SevenFa AI 中转站:低成本接入 630+ 模型的实战指南

在开发 AI 应用时,最让人头疼的往往不是算法本身,而是如何在一个碎片化的模型生态中找到最优解。今天接了个需求要用最新的推理模型,明天那个模型降价了想切换,后天又发现另一个渠道的延迟更低。为了适配不同厂商的 API 格式&…

作者头像 李华
网站建设 2026/6/10 13:49:00

分布式系统架构:分布式事务与最终一致性的工程实践

分布式系统架构:分布式事务与最终一致性的工程实践一、分布式事务的现实困境:一致性不是免费的 在单体架构中,事务由数据库的 ACID 机制保证,开发者几乎不需要关心一致性问题。但进入微服务架构后,一个业务操作往往跨越…

作者头像 李华