0赞
赞赏
更多好文
本文基于真实项目迁移经验,含泪总结。如果你正准备将 LiveData 重构为 Flow,这篇能帮你省下三天加班时间。
引言:为什么迁移?又为何“踩坑”?
随着 Jetpack Compose 成为主流、Kotlin 协程生态成熟,Google 官方多次建议:新项目优先使用 StateFlow/SharedFlow,LiveData 仅用于与旧架构兼容。
我们团队在重构核心模块时,满怀信心地开启迁移——直到屏幕旋转后日志刷屏、内存飙升、App 闪退……
今天,就用血泪经验告诉你:迁移不是 Ctrl+H 替换,而是思维模式的升级。
🕳️ 坑 1:生命周期绑定失效——“自动感知”变“手动管理”
❌ 错误示范
// Fragment 中天真写法
lifecycleScope.launch {
viewModel.userFlow.collect { updateUserUI(it) } // 旋转屏幕?日志爆炸!
}
现象:屏幕旋转后收集逻辑重复触发;退出页面后协程仍在后台运行,内存泄漏!
🔍 原因
LiveData 的 observe() 内部自动绑定 LifecycleOwner,DESTROYED 时自动解绑。
而 Flow 的 collect 是纯协程操作,启动即持续运行,除非手动取消。
✅ 正确姿势(AndroidX Lifecycle ≥ 2.4.0)
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) { // 推荐 STARTED(含 onResume)
viewModel.userFlow.collect { updateUserUI(it) }
}
}
- 原理:
repeatOnLifecycle会在生命周期低于指定状态时自动取消协程,恢复时重建收集。 - 💡 小贴士:旧项目可用
flowWithLifecycle(需androidx.lifecycle:lifecycle-runtime-ktx:2.6.0+):viewModel.userFlow .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .collect { ... }
🕳️ 坑 2:线程调度迷局——“我以为在主线程”
❌ 错误示范
// ViewModel 中
val userData = flow { emit(repository.fetchUser()) } // 无调度声明!
// Fragment 中
lifecycleScope.launch { // 默认主线程
viewModel.userData.collect { showName(it.name) } // 崩溃:NetworkOnMainThreadException!
}
🔍 原因
LiveData 的 liveData{} 构建器自动切换至 IO 线程,发射前切回主线程。
Flow 的 flow{} 继承调用协程的上下文!若在主线程启动收集,发射逻辑也在主线程执行。
✅ 正确姿势
// ViewModel:明确指定上游线程
val userData = flow { emit(repository.fetchUser()) }
.flowOn(Dispatchers.IO) // 仅影响 upstream(发射端)
.catch { emit(UserError(it)) } // 后文详述
// Fragment:collect 块天然在主线程(因 lifecycleScope.launch 默认主线程)
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.userData.collect { /* 安全更新 UI */ }
}
}
- 💡 关键:
flowOn只改上游线程,不影响 collect 块执行线程。确保 collect 在主线程协程中启动即可。
🕳️ 坑 3:重复收集陷阱——RecyclerView 里的“幽灵协程”
❌ 错误示范
// Adapter onBindViewHolder
holder.lifecycleOwner.lifecycleScope.launch {
getItemFlow(position).collect { holder.bind(it) } // 滚动即爆炸!
}
现象:快速滑动列表,日志疯狂输出,内存持续上涨,ANR 预警!
🔍 原因
RecyclerView 复用 ViewHolder 时,旧协程未取消,新协程又启动,导致同一 item 多协程并发收集。
✅ 正确姿势
- 首选方案:重构数据流!在 ViewModel 中将列表整体转为 StateFlow:
Adapter 监听整个列表变化,避免 item 级别收集。val userListState: StateFlow<List<User>> = _userList - 不得已时:用 View Tag 管理 Job(不推荐,仅应急):
// onBindViewHolder val job = holder.itemView.getTag(R.id.flow_job) as? Job job?.cancel() val newJob = lifecycleScope.launch { ... } holder.itemView.setTag(R.id.flow_job, newJob) // onViewRecycled 中取消
🕳️ 坑 4:状态持有者迷失——“我的 value 去哪了?”
❌ 错误示范
// 迁移前(LiveData)
if (userLiveData.value != null) loginButton.isEnabled = true
// 迁移后(普通 Flow)
if (userFlow.value != null) ... // 编译错误!Flow 无 value 属性
🔍 原因
LiveData 是状态持有者(缓存最新值);普通 Flow 是冷流(无状态,每次 collect 重执行)。
✅ 正确姿势:用 StateFlow 替代
// ViewModel
private val _userState = MutableStateFlow<User?>(null)
val userState: StateFlow<User?> = _userState
// 外部安全读取当前值
if (userState.value != null) { ... }
// 更新值(自动去重,避免重复发射)
_userState.value = newUser
- 💡 注意:StateFlow 默认通过
equals去重。需发射重复值?考虑SharedFlow(replay = 1)+onSubscription。
🕳️ 坑 5:错误处理缺失——“Flow 崩了,App 也崩了!”
❌ 错误示范
lifecycleScope.launch {
viewModel.userFlow.collect { ... } // 网络异常 → 协程崩溃 → App 闪退!
}
🔍 原因
LiveData 内部吞掉异常;Flow 的异常会向上传播至协程,未捕获则触发 UncaughtExceptionHandler。
✅ 正确姿势:错误即数据
// Repository 层:将异常转化为 UI 状态
sealed class UserResult {
data class Success(val user: User) : UserResult()
data class Error(val msg: String) : UserResult()
}
val userFlow = flow {
emit(UserResult.Success(repository.fetchUser()))
}.catch { emit(UserResult.Error(it.message ?: "未知错误")) }
.flowOn(Dispatchers.IO)
// UI 层:统一处理状态
collect { result ->
when (result) {
is UserResult.Success -> showUser(result.user)
is UserResult.Error -> showToast(result.msg)
}
}
- 💡 优势:避免协程崩溃,UI 处理逻辑清晰,测试更友好。
🌟 迁移心法总结
| 维度 | LiveData | Flow(正确用法) | 关键动作 |
|---|---|---|---|
| 生命周期 | 自动绑定 | repeatOnLifecycle / flowWithLifecycle | 必须显式管理 |
| 线程 | 构建器自动调度 | flowOn + 明确 collect 线程 | 声明式调度 |
| 状态持有 | 有 .value | 用 StateFlow | 选对流类型 |
| 错误处理 | 内部吞掉 | 转为数据状态 / catch 操作符 | 错误即数据 |
| 列表场景 | 单值观察 | 整体 StateFlow + DiffUtil | 避免细粒度收集 |
最后说两句
迁移不是目的,提升架构清晰度与可维护性才是。
Flow 的强大在于组合性(map/combine/retry)、与 Compose 的无缝契合、以及更符合 Kotlin 协程哲学的设计。
记住三句话:
1️⃣ 状态用 StateFlow,事件用 SharedFlow
2️⃣ 收集必绑生命周期,线程必显式声明
3️⃣ 错误不抛异常,而是发射状态
踩坑不可怕,可怕的是重复踩坑。希望这篇能成为你迁移路上的“防坑地图” 🗺️
你在迁移中还遇到过哪些坑?欢迎评论区分享! 👇
本文代码基于 Kotlin 1.8+、AndroidX Lifecycle 2.6.1、Coroutines 1.7.1 验证
原创不易,转载请注明出处 ✨
