Kotlin Flow 已经成为 Android 里处理异步数据流的主流方案,它建立在 Kotlin 协程之上,常被用来替代部分 RxJava 和 LiveData 场景。很多初学者真正卡住的地方,不是会不会写 collect,而是分不清什么时候该用普通 Flow,什么时候该换成 StateFlow 或 SharedFlow。
这篇文章按“数据从哪里来、经过什么处理、最终怎么安全落到 UI”这条线来梳理。你可以据此判断三种 Flow 类型的职责边界,也能顺手把生命周期收集和常用操作符放到合适的位置。
先理解 Flow 的数据管道模型
把 Flow 看成一条异步数据管道会更直观,整个过程通常可以分成三段:
- 上游(Upstream):负责生产数据,例如数据库读取、网络请求。
- 中游(Intermediaries):负责处理数据,例如用
filter过滤、用map转换。 - 下游(Downstream):负责接收和消费数据,例如更新 UI。
这个划分的价值在于:当你排查性能、线程切换或生命周期问题时,能很快判断问题出在“数据源”“处理中间层”还是“界面消费层”。
冷流与热流,到底差在哪里
Flow 体系里最核心的一组概念,就是冷流和热流。

| 特性 | Cold Flow(普通 Flow) | Hot Flow(StateFlow / SharedFlow) |
|---|---|---|
| 激活时机 | 只有被收集(collect)时才开始运行 | 无论有无收集者,它都在内存中活跃,行为上类似 LiveData |
| 数据生产 | 一对一(Unicast),每个收集者都会重新触发一次数据生产流程 | 一对多(Multicast),多个收集者共享同一个数据源 |
| 典型场景 | 耗时数据库读取、文件下载、简单异步转换 | UI 状态管理(StateFlow)、一次性事件(SharedFlow) |
如果你只记一个判断标准,可以记这一句:需要“每次收集都重新执行”的,用普通 Flow;需要“值或事件在内存里持续存在并可被多个观察者共享”的,用热流。
Flow、StateFlow、SharedFlow 分别适合什么场景
普通 Flow:适合按需执行的异步任务
普通 Flow 是最基础的类型,适合执行耗时任务,并按顺序发射多个值。它的特点是“没人收集就不运行”,因此非常适合那些明确由下游触发的工作流。
// 例子:倒计时
fun countdownFlow(): Flow = flow {
for (i in 5 downTo 0) {
emit(i) // 发射数据
delay(1000) // 挂起函数,不阻塞线程
}
}
像倒计时、一次数据库查询、一次文件下载这类逻辑,用普通 Flow 很顺手,因为每次进入收集流程都会完整执行一遍上游逻辑。
StateFlow:适合持续保存 UI 状态
StateFlow 可以看作是 LiveData 的现代替代品,特别适合 ViewModel 中的状态管理。
- 特点:始终持有最新的一个状态值。
- 收集行为:新的收集者加入时,会立刻收到当前最新值。
- 使用场景:保存 UI 状态,例如
Loading、Success、Error。 - 和 LiveData 的区别:
StateFlow必须有初始值,同时完整支持协程操作符。
// ViewModel 中
private val _uiState = MutableStateFlow("Initial State")
val uiState: StateFlow = _uiState.asStateFlow()
// 更新状态
_uiState.value = "New State"
如果一个值代表“当前界面此刻应该显示什么”,那它大概率应该是 StateFlow,而不是普通 Flow。
SharedFlow:适合一次性事件分发
SharedFlow 更适合发送一次性事件,也就是那些“发生过就算处理完”的消息。
- 特点:用于发送一次性事件(One-off events)。
- 默认行为:默认不保留旧数据,除非额外配置
replay。 - 收集行为:新收集者不会收到过去已经发出的事件。
- 典型场景:导航跳转、弹出 Toast、显示 Snackbar、服务器推送消息。
// ViewModel 中
private val _events = MutableSharedFlow()
val events: SharedFlow = _events.asSharedFlow()
// 发送事件 (suspend function)
_events.emit("Show Toast")
一个常见误区是把 Toast、页面跳转这类事件塞进 StateFlow。这样很容易在界面重建后重复消费旧值。遇到这类“一次触发、一次处理”的需求,通常应该优先考虑 SharedFlow。
在 UI 层怎么安全收集 Flow
Flow 好用,但前提是收集方式要和生命周期对齐。否则页面退到后台后仍在收集,就很容易造成资源浪费,甚至引发界面更新时机错误。

Activity / Fragment:用 repeatOnLifecycle
在传统 Views 体系里,推荐使用 repeatOnLifecycle。它会在生命周期达到指定状态时开始收集,在状态低于该值时自动取消协程。
最常见的写法是使用 Lifecycle.State.STARTED:进入前台开始收集,进入 STOPPED 后停止接收数据。
// 在 Activity 或 Fragment 中
lifecycleScope.launch {
// 只有当生命周期至少是 STARTED 时才执行块内代码
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 这两个流会并行收集
launch {
viewModel.uiState.collect { state ->
// 更新 UI
}
}
launch {
viewModel.events.collect { event ->
// 处理事件
}
}
}
}
这里把 uiState 和 events 分别放进两个 launch 中并行收集,是非常常见也非常实用的模式。
Compose:用 collectAsStateWithLifecycle()
在 Jetpack Compose 中,通常直接使用 collectAsStateWithLifecycle()。它会把 Flow 转成 Compose 可用的 State,同时感知生命周期,减少手动处理的负担。
@Composable
fun MyScreen(viewModel: MyViewModel) {
// 需要引入依赖: androidx.lifecycle:lifecycle-runtime-compose
val state by viewModel.uiState.collectAsStateWithLifecycle()
Text(text = state)
}
如果页面展示的是持续变化的状态,Compose + collectAsStateWithLifecycle() 基本就是当前最顺手的组合。
常用操作符该怎么理解
Flow 的强大之处,不只在于发数据,还在于可以沿着数据流一路加工。下面这些操作符最常见:
转换类:改造数据结构
map:做数据映射。transform:做更灵活的自定义转换。
过滤类:减少无效更新
filter:按条件过滤。debounce:防抖,搜索框场景很常见。distinctUntilChanged:过滤重复值;StateFlow默认自带这一特性。
组合类:处理多个数据源
combine:合并多个流。zip:按配对方式组合多个流。flatMapLatest:切换流,适合“搜索词变化时取消旧请求、发起新请求”这类场景。
异常处理与线程切换
catch:捕获上游异常。flowOn(Dispatchers.IO):指定上游代码在 IO 线程执行。
一个实用理解方式是:catch 和 flowOn 往往都更偏“上游控制”;UI 层应该尽量专注于收集结果,而不是承担过多数据加工责任。
最后总结:三种 Flow 类型怎么选
- 需要按需触发、每次收集都重新执行任务:选普通
Flow。 - 需要持续保存并分发最新 UI 状态:选
StateFlow。 - 需要发送导航、Toast、Snackbar 这类一次性事件:选
SharedFlow。
如果再配合 repeatOnLifecycle 或 collectAsStateWithLifecycle() 做生命周期感知收集,Flow 在 Android 项目里的可维护性会明显高于很多早期的异步方案。







