LiveData 到 Flow 的迁移:我踩过的 5 个坑

1 阅读3分钟

在 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 反而会让系统更复杂。