LiveData 到 Flow 的迁移:我踩过的 5 个坑(附避坑指南)

avatar
蓝猫IP属地:上海
02026-02-08:11:56:16字数 4587阅读 1

本文基于真实项目迁移经验,含泪总结。如果你正准备将 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 多协程并发收集。

✅ 正确姿势

  1. 首选方案:重构数据流!在 ViewModel 中将列表整体转为 StateFlow:
    val userListState: StateFlow<List<User>> = _userList
    
    Adapter 监听整个列表变化,避免 item 级别收集。
  2. 不得已时:用 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 处理逻辑清晰,测试更友好。

🌟 迁移心法总结

维度LiveDataFlow(正确用法)关键动作
生命周期自动绑定repeatOnLifecycle / flowWithLifecycle必须显式管理
线程构建器自动调度flowOn + 明确 collect 线程声明式调度
状态持有.valueStateFlow选对流类型
错误处理内部吞掉转为数据状态 / 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 验证
原创不易,转载请注明出处

总资产 0
暂无其他文章

热门文章

暂无热门文章