在 Android 项目中从 LiveData 迁移到 Kotlin Flow,看起来像是一次“顺理成章的技术升级”,但真正落地后,我才意识到:
Flow 不是 LiveData 的 1:1 替代,而是一整套响应式模型的变化。
下面记录我在真实项目迁移过程中踩过的 5 个典型坑,以及最终的解决方式。
坑一:把 Flow 当成“更高级的 LiveData”
当时的错误认知
一开始我几乎是“机械迁移”:
val userLiveData: LiveData<User> ↓ val userFlow: Flow<User>
ViewModel 暴露 Flow,UI collect,看起来一切正常。
实际问题
- LiveData 是 生命周期感知 + 热数据
- Flow 默认是 冷流
结果是:
- 每次
collect都重新执行一遍上游逻辑 - 网络请求、数据库查询被重复触发
本质原因
LiveData:数据源一直存在,UI 只是订阅
Flow:UI 订阅 = 触发数据生产
我的修正方式
对于“状态型数据”,不直接暴露普通 Flow,而是:
val userState: StateFlow<User>
并在 ViewModel 中:
stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = User.Empty )
结论:
Flow ≠ LiveData,
LiveData 更接近的是StateFlow/SharedFlow
坑二:collect 写在错误的生命周期里
常见写法(我一开始也这么干)
lifecycleScope.launch { viewModel.flow.collect { ... } }
问题表现
- 页面退到后台,Flow 仍在收集
- 重进页面,出现重复 collect
- 某些场景下 UI 被更新多次
根因
Flow 不自动感知生命周期,不像 LiveData 那样“活着就收,死了就停”。
正确姿势
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.flow.collect { ... } } }
或者更常见的:
viewModel.flow .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) .onEach { ... } .launchIn(lifecycleScope)
结论:
用 Flow,不手动处理生命周期 = 迟早踩坑
坑三:事件被重复消费(Toast / 跳转 / Dialog)
LiveData 时代
我用的是:
- SingleLiveEvent
- Event 包装类
迁移到 Flow 后的天真想法
val eventFlow = MutableStateFlow<Event?>(null)
结果:
- 旋转屏幕后,事件又执行了一次
- 页面重进,Toast 又弹了
根因
StateFlow 会 重放最新值,
而 UI 事件本质是:只消费一次。
正确解法
用 SharedFlow,明确语义:
val eventFlow = MutableSharedFlow<UiEvent>( replay = 0, extraBufferCapacity = 1 )
发送事件:
eventFlow.tryEmit(UiEvent.ShowToast)
结论:
状态 → StateFlow
事件 → SharedFlow
不要混用
坑四:combine 后的“无意义 UI 刷新”
场景
多个数据源组合 UI:
combine(flowA, flowB) { a, b -> UiState(a, b) }
问题
- flowA 频繁变化
- flowB 实际没变
- UI 却每次都刷新
原因
Flow 不会帮你判断:
“新值和旧值是否等价”
解决方式
在合适的地方加:
.distinctUntilChanged()
或者对组合后的 UiState 做 equals 控制。
结论:
Flow 很“诚实”,
你不拦,它就一直发
坑五:在 Flow 里直接写耗时逻辑,线程炸了
LiveData 时代的错觉
以前 postValue 很少关心线程。
Flow 里我一开始的写法
flow { emit(repository.loadBigData()) }
结果:
- collect 在 Main
- 上游也在 Main
- UI 卡顿,甚至 ANR
正确理解
Flow 的线程 ≠ collect 的线程
上游执行在哪,全看你有没有切
修正
flow { emit(repository.loadBigData()) }.flowOn(Dispatchers.IO)
结论:
Flow 默认不帮你切线程
每个耗时点,都要显式思考调度器
最后的整体认知变化
迁移完成后,我对 LiveData 和 Flow 的理解是:
- LiveData:简单、保守、UI 友好
- Flow:强大、灵活,但需要你承担设计成本
如果项目:
- 状态简单
- 数据源单一
LiveData 并不落后
如果项目:
- 多数据源
- 有组合 / 转换 / 背压需求
Flow 才是真正的优势区
总结一句话
LiveData → Flow,不是语法迁移,而是心智模型迁移。
如果你只是“为了用而用”,
Flow 反而会让系统更复杂。