第一部分:Flow 基础概念
第1章:初识 Kotlin Flow
快速入门:最简单的 Flow 示例(collect 为挂起函数,需在协程内调用,如 runBlocking 或 scope.launch)。
runBlocking {
flowOf(1, 2, 3)
.map { it * 2 }
.collect { println(it) } // 输出 2, 4, 6
}
Flow 数据流完整链路:
[数据源] → flow/flowOf/asFlow
↓
[操作符链] → map/filter/debounce/flatMapLatest/shareIn|stateIn/catch...
↓
[消费] → collect/collectLatest/launchIn
shareIn/stateIn 在操作符链中,多订阅共享时插入;冷流经其转为热流后,下游多处 collect 共享同一数据源。
1.1 Flow 是什么:官方定义与设计哲学
官方定义:
Kotlin Flow 是 kotlinx.coroutines 中用于处理异步数据流的 API。它是一个基于协程构建的响应式流处理框架,专为异步、按顺序、连续的数据流设计。Flow 与协程深度集成,提供声明式、可组合的方式处理随时间变化的值序列。
设计哲学:
- 声明式编程:使用函数式操作符表达数据转换,而不是命令式控制流
- 结构化并发:与协程作用域深度绑定,自动管理资源生命周期
- 冷流优先:默认采用按需生产、零共享的冷流模型,确保资源安全
- 可组合性:操作符可链式组合,构建复杂的数据处理管道
- 协程原生:无缝集成协程上下文,支持挂起函数和结构化取消
1.2 Flow 的生态位:对比 LiveData、SharedFlow、StateFlow、Channel
五者关系简图:
┌─ Flow(冷流)──── 按需、独立
│
数据流 ─ ┼─ SharedFlow(热流)─ 事件、广播、可配 replay
│
├─ StateFlow(热流)─ SharedFlow 特化,状态、必有初始值
│
├─ LiveData(Android)─ 生命周期感知,传统 UI
│
└─ Channel ── 点对点,单次消费,协程间通信
| 特性 | Flow (冷流) | SharedFlow (热流) | StateFlow (热流) | LiveData | Channel |
|---|---|---|---|---|---|
| 数据模型 | 连续数据流 | 事件流/广播 | 状态容器 | 生命周期感知数据持有者 | 点对点通信管道 |
| 温度 | 冷流 | 热流 | 热流 | 热流 | 热流 |
| 生命周期 | 依赖收集者 | 独立于收集者 | 独立于收集者 | 绑定观察者生命周期 | 独立于收发者 |
| 数据共享 | 每个收集者独立数据 | 多收集者共享数据 | 多观察者共享状态 | 多观察者共享数据 | 单次消费 |
| 初始值 | 无 | 可选(通过 replay) | 必须有 | 可有可无 | 无 |
| 背压处理 | 内置(协程挂起) | 可配置缓冲区策略 | 最新值替换 | 无 | 可配置缓冲区 |
| 适用场景 | 网络请求、数据库查询 | 事件总线、实时数据广播 | UI 状态管理 | Android UI 状态 | 协程间通信 |
1.3 Flow 的核心价值:为什么 Google 将其作为异步首选?
四大核心价值:
-
协程原生:
- 无缝集成协程作用域,自动处理取消和资源清理
- 支持挂起函数,可直接在流操作中使用异步操作
-
结构化并发安全:
viewModelScope.launch {
repository.dataFlow()
.onEach { updateUI(it) }
.launchIn(this) // 自动绑定到 viewModelScope
}
- 声明式与可组合性:
searchFlow
.debounce(300)
.filter { it.length > 2 }
.distinctUntilChanged()
.flatMapLatest { query ->
flow { emit(api.search(query)) } // api.search 返回 List 等,整份发射
}
.catch { emit(SearchResult.Error(it)) }
- 平台无关性:
- 纯 Kotlin 实现,可在 Android、iOS、后端等多平台使用
- 摆脱 Android 特定框架(如 LiveData)的依赖
1.4 Flow 的三大基石:基于协程、冷流特性、顺序执行
基石一:基于协程
- 所有 Flow 操作都在协程上下文中执行
- 支持挂起函数,可安全执行 I/O 操作
- 自动传播取消信号
基石二:冷流特性(默认)
val coldFlow = flow {
println("开始生产数据") // 只在收集时执行
emit(1)
emit(2)
emit(3)
}
// 多次收集会多次执行
coroutineScope {
launch { coldFlow.collect { println(it) } } // 输出:开始生产数据
launch { coldFlow.collect { println(it) } } // 再次输出:开始生产数据
}
基石三:顺序执行
- 每个元素按顺序通过整个处理链
- 操作符按声明顺序执行
- 保证数据处理的一致性
1.5 Flow 的三大定律:上下文保留、异常透明性、数据顺序保证
定律一:上下文保留
Flow 构建器中的代码运行在调用者的上下文中,除非使用 flowOn 显式切换:
flow {
// 这里的上下文是调用者的上下文
emit(1)
}.flowOn(Dispatchers.IO) // 切换上游上下文
.collect {
// 这里恢复为原始上下文
}
定律二:异常透明性
异常必须通过 catch 操作符处理,或向上游传播:
flow {
throw RuntimeException("错误")
}.catch { cause ->
// 捕获异常并恢复
emit(RecoveryValue)
}.collect()
定律三:数据顺序保证
在没有并发操作符(如 buffer)的情况下,数据按顺序处理:
flowOf(1, 2, 3)
.map { it * 2 } // 1→2, 2→4, 3→6
.filter { it > 3 } // 2 被过滤,4 通过,6 通过
.collect { print(it) } // 输出:4 6(顺序保持不变)
小结:Flow 基于协程、默认为冷流、元素顺序保证;异常用 catch、上下文用 flowOn。
第二部分:冷流与热流
第2章:冷与热:Flow 的两种形态
2.1 理解数据流的「温度」
核心差异:
- 冷流:按需生产,每个收集者获得独立的数据流
- 热流:主动生产,多个收集者共享相同的数据源
温度类比:
- 冷流 像「视频点播」:每个观众独立观看,从头开始播放
- 热流 像「电视直播」:所有观众同时观看相同的内容
冷流 vs 热流 对比图:
┌─────────────────────────────────────┬─────────────────────────────────────┐
│ 冷流 (Cold) │ 热流 (Hot) │
├─────────────────────────────────────┼─────────────────────────────────────┤
│ collect₁ ──→ 生产₁ ──→ 数据₁ │ 生产 ──→ 共享缓冲区 │
│ collect₂ ──→ 生产₂ ──→ 数据₂ │ │ │
│ collect₃ ──→ 生产₃ ──→ 数据₃ │ collect₁──┼──→ 数据 │
│ (每订阅独立生产) │ collect₂──┘ │
│ │ collect₃──→ 数据 (共享) │
├─────────────────────────────────────┼─────────────────────────────────────┤
│ 按需、懒加载、取消即停 │ 持续、多订阅共享、生产独立于消费 │
└─────────────────────────────────────┴─────────────────────────────────────┘
2.2 冷流(Cold Flow)本质解析
核心定义:冷流是 Flow 的默认形态。它像一个「懒人配方」——只有在有人订阅(collect)时才开始执行生产逻辑;每次订阅都会触发一次独立的生产过程;订阅取消时,生产也随之停止。
冷流有三大本质特征:
2.2.1 惰性执行(Lazy)
含义:定义 flow 时,内部的 emit、网络请求、数据库查询等逻辑不会立刻执行,只有调用 collect 时才会真正开始。
意义:避免无谓计算。若定义后从不收集,则不会有任何资源消耗。
val coldFlow = flow {
println("开始生产") // 定义时不会执行
for (i in 1..3) {
delay(100)
emit(i)
}
}
// 只有 collect 时才执行(scope 为 CoroutineScope,如 viewModelScope)
scope.launch { coldFlow.collect { println(it) } } // 此时才输出 "开始生产"
2.2.2 点对点传输(每个收集者独立副本)
含义:同一个冷流被多次 collect 时,每次都会从头执行一遍生产逻辑,各收集者得到的是彼此独立的数据序列,互不影响。
意义:适合「每次请求都要重新计算」的场景(如网络请求、查询数据库),不会出现多个 UI 共享同一份过时数据的问题。
val dataFlow = flow {
repeat(3) {
delay(100)
emit(System.currentTimeMillis())
}
}
coroutineScope {
launch { dataFlow.collect { println("A: $it") } }
launch { dataFlow.collect { println("B: $it") } }
}
// A 和 B 会各自收到 3 个不同的时间戳,因为生产逻辑执行了 2 次
2.2.3 生命周期与收集者绑定
含义:冷流的生产协程由 collect 所在的协程作用域驱动。收集被取消时,生产协程会被取消,finally 块会执行,便于做资源清理。
意义:可以在 flow 内部安全地持有数据库连接、文件句柄等,收集结束或取消时自动释放。
fun observeUserData(userId: String): Flow<UserData> = flow {
val connection = openDatabaseConnection()
try {
while (true) {
emit(fetchUserData(userId))
delay(1000)
}
} finally {
connection.close() // collect 取消时自动清理
}
}
与热流的本质区别:热流的生产是独立于收集者的,不会因为「没人订阅」而停止;冷流则完全相反——没人收集就不生产,收集取消就停止。
2.3 热流(Hot Flow)特性详解
核心定义:热流(SharedFlow、StateFlow)的生产与消费是解耦的。生产者独立运行,不依赖是否有收集者;数据发出后存入共享缓冲区,多个收集者从同一份数据源接收。就像「电视直播」——节目在播,观众随时加入、随时离开,看到的是同一时段的内容。
热流有三大本质特征:
2.3.1 独立存在性(生产不依赖收集者)
含义:生产者独立于收集者运行。可以先启动生产,再启动收集;也可以在生产进行中,随时有新收集者加入。生产不会因为「没人订阅」而暂停。
意义:适合需要持续运行的场景(传感器、WebSocket、全局状态),数据始终在流动,收集者按需订阅即可。
val hotFlow = MutableSharedFlow<Int>()
scope.launch {
// 生产者立刻开始,不等人订阅
repeat(10) {
delay(100)
hotFlow.emit(it)
}
}
// 延迟 300ms 才订阅,仍能收到 3~9 的数据(0~2 已错过)
scope.launch {
delay(300)
hotFlow.collect { println("收到: $it") }
}
2.3.2 广播机制(一发多收)
含义:一次 emit 发出的数据会同时送达所有当前订阅者,每个订阅者都能收到同一份数据。新订阅者加入时,根据 replay 配置决定能否收到历史数据。
意义:适合事件总线、通知推送——一处发送,多处响应。
val eventBus = MutableSharedFlow<Event>(replay = 0)
scope.launch { eventBus.emit(Event("USER_LOGIN")) }
// 多个收集者同时订阅,都会收到后续的同一事件
scope.launch { eventBus.collect { handleEvent(it) } }
scope.launch { eventBus.collect { logEvent(it) } }
2.3.3 多订阅者共享同一数据源
含义:多个收集者订阅同一个热流时,共享同一份生产逻辑和缓冲区。生产者只执行一次,发出的数据被复制给每个订阅者,而不是像冷流那样「每个收集者触发一次独立生产」。
意义:节省资源。传感器数据、实时股价等只需生产一次,多个 UI 组件共享。
class SensorManager(private val scope: CoroutineScope) {
private val _sensorData = MutableSharedFlow<SensorData>(replay = 1)
val sensorData = _sensorData.asSharedFlow()
fun startMonitoring() {
scope.launch {
while (true) {
_sensorData.emit(readSensor()) // 只读一次传感器
delay(100)
}
}
}
}
// 多个界面同时订阅,共享同一份传感器数据
scope.launch { sensorData.collect { updateScreenA(it) } }
scope.launch { sensorData.collect { updateScreenB(it) } }
与冷流的本质区别:冷流是「按需生产、每订一份产一份」;热流是「持续生产、多订共享一份」。热流适合「状态/事件」场景,冷流适合「请求/查询」场景。
2.4 Flow 的三元模型
三元模型指 Flow 数据流中的三个角色:生产者(用 flow {}、emit() 等产生数据)→ 操作符(map、filter 等组成处理管道)→ 消费者(collect 等接收并处理数据)。数据按此顺序流动。
┌─────────────┐ ┌─────────────────────────────┐ ┌─────────────┐
│ 生产者 │ │ 操作符(可链式) │ │ 消费者 │
│ flow/emit │ ──→ │ map→filter→debounce→... │ ──→ │ collect │
└─────────────┘ └─────────────────────────────┘ └─────────────┘
│ │ │
产生原始数据 转换、过滤、组合 接收并处理
// 生产 → 处理 → 消费
flow { listOf(1, 2, 3).forEach { emit(it) } }
.filter { it > 1 }
.map { "值: $it" }
.collect { updateUI(it) } // 输出 "值: 2", "值: 3"
2.5 典型场景选择策略
一、StateFlow 场景(UI 状态)
| 场景 | 推荐选择 | 选择理由 | 配置要点 |
|---|---|---|---|
| UI 状态管理 | StateFlow | 必有初始值、自动去重,新订阅者立即可见 | 初始值 + Eagerly(全局)/ Lazily(页面)/ WhileSubscribed |
| 页面级数据(按需加载) | 冷流 + stateIn(Lazily) | 进入页面才加载,离开可释放 | stateIn(SharingStarted.Lazily) |
二、SharedFlow 场景(事件 / 实时推送)
| 场景 | 推荐选择 | 选择理由 | 配置要点 |
|---|---|---|---|
| 事件总线 / 一次性事件 | SharedFlow | 无重播,不堆积,一发即过 | replay=0,onBufferOverflow=SUSPEND |
| Toast / Snackbar 等一次性 UI 提示 | SharedFlow | 事件型,不重播,多界面可监听 | replay=0 |
| 实时数据推送(股价、传感器) | SharedFlow | 持续生产、多订阅共享,新订阅者需最新值 | replay=1,WhileSubscribed |
三、冷流 Flow 场景(请求 / 查询)
| 场景 | 推荐选择 | 选择理由 | 配置要点 |
|---|---|---|---|
| 网络请求 / API 调用 | 普通 Flow(冷流) | 每次收集重新请求,按需执行 | flow {} + flowOn(IO) + catch |
| 数据库查询 / 分页加载 | 普通 Flow(冷流) | 每次进入页面重新查询,取消即停止 | flow {} 或 Room flow 扩展 |
| 资源敏感 / 需及时释放 | 冷流 | 无人收集不执行,收集取消即释放 | flow {} + finally 清理 |
四、冷流 + 热流转换场景
| 场景 | 推荐选择 | 选择理由 | 配置要点 |
|---|---|---|---|
| 搜索建议 / 防抖输入 | 冷流 + shareIn | 冷流防抖后转热流共享结果 | debounce + flatMapLatest + shareIn(replay=1) |
| 多订阅者共享同一数据 | 热流(SharedFlow/StateFlow) | 生产一次、多人消费,省资源 | 冷流转热流用 shareIn / stateIn |
快速记忆:状态用 StateFlow,事件用 SharedFlow(replay=0),请求/查询用冷流,实时共享用 SharedFlow(replay=1)。
场景选择流程图:
需要处理什么数据?
│
▼
┌───────────────┐
│ 是 UI 状态? │──是──→ StateFlow
└───────────────┘
│ 否
▼
┌───────────────┐
│ 是一次性事件? │──是──→ SharedFlow(replay=0)
└───────────────┘
│ 否
▼
┌───────────────┐
│ 是多订阅共享? │──是──→ shareIn/stateIn 转热流
└───────────────┘
│ 否
▼
┌───────────────┐
│ 是请求/查询? │──是──→ 冷流 Flow
└───────────────┘
│ 否
▼
参考 2.5 典型场景表
2.6 转换魔法:shareIn / stateIn
将冷流转为热流,使多订阅者共享同一份数据,避免重复执行生产逻辑。
1. 为什么需要转换?
问题:冷流每次 collect 都会重新执行生产逻辑。若标题栏、详情区、侧边栏都要用户信息,各自 collect 会触发 3 次网络请求,且可能不同步。
解决:用 shareIn/stateIn 转成热流,上游只在一个协程里收集一次,下游多订阅者共享结果。
冷流:collect₁ → 请求₁ collect₂ → 请求₂ collect₃ → 请求₃
热流:collect₁ ─┐
├→ 一次请求 → 共享给 collect₁、collect₂、collect₃
collect₂ ─┤
collect₃ ─┘
// 冷流:每个 collect 都请求
val cold: Flow<User> = flow { emit(api.getUser()) }
// 热流:只请求一次
val hot = flow { emit(api.getUser()) }
.shareIn(scope, SharingStarted.Lazily, replay = 1)
2. 核心概念
| 函数 | 返回类型 | 作用 |
|---|---|---|
| shareIn | SharedFlow | 冷流 → 热流,replay 可配 |
| stateIn | StateFlow | 冷流 → 状态流,必有初始值,自动去重 |
3. 选 shareIn 还是 stateIn?
| 对比项 | shareIn | stateIn |
|---|---|---|
| 用途 | 事件流、需自定义 replay | UI 状态、当前值 |
| 初始值 | 无 | 必须有 |
| replay | 可配,默认 0 | 固定 1 |
| 自动去重 | 无 | 有 |
口诀:当前状态 → stateIn;事件或需 replay≠1 → shareIn。
shareIn / stateIn 选择流程图:
冷流需多订阅共享?
│
├──否──→ 保持冷流
│
└──是──→ 需要「当前状态」+ 自动去重?
│
├──是──→ stateIn
│
└──否──→ 需要 replay=0(事件)或 replay=N?
│
└──→ shareIn
4. 参数说明
shareIn:
Flow<T>.shareIn(
scope: CoroutineScope, // 收集上游的作用域
started: SharingStarted, // 何时开始/停止
replay: Int = 0 // 新订阅者收到最近 N 个值
): SharedFlow<T>
stateIn:
Flow<T>.stateIn(
scope: CoroutineScope,
started: SharingStarted,
initialValue: T // 必须有
): StateFlow<T>
SharingStarted:
| 策略 | 行为 | 适用 |
|---|---|---|
| Eagerly | 立即开始,不停止 | 全局配置、主题 |
| Lazily | 首个订阅时开始,之后持续 | 页面数据、按需加载 |
| WhileSubscribed(stop, expire) | 有订阅运行,无订阅延迟停止 | 页面切换频繁 |
SharingStarted 生命周期示意:
Eagerly: 创建 ──→ [======== 一直运行 ========] ──→ scope 销毁
Lazily: 创建 ──→ 等订阅 ──→ [=== 运行 ===] ──→ scope 销毁
WhileSub: 创建 ──→ 等订阅 ──→ [运行] ──→ 无订阅 ──→ 延迟 stop ──→ 停止
| WhileSubscribed 参数 | 默认 | 含义 |
|---|---|---|
| stopTimeoutMillis | 0 | 最后订阅离开后多久停止 |
| replayExpirationMillis | Long.MAX_VALUE | 停止后多久清除 replay 缓存(0=立即) |
5. 场景配置示例
| 场景 | 配置 | 说明 |
|---|---|---|
| UI 状态 | stateIn(Eagerly, init) | 立即有值,持续更新 |
| 页面数据 | stateIn(Lazily, init) | 有订阅才加载 |
| 事件总线 | shareIn(Eagerly, replay=0) | 不重播,一发即过 |
| 实时数据 | shareIn(WhileSubscribed(5s), replay=1) | 新订阅拿最新,离开 5s 后停 |
| 一次性数据 | 不转换 | 保持冷流 |
// UI 状态
val uiState = repo.dataFlow().stateIn(vmScope, SharingStarted.Eagerly, UiState())
// 页面数据
val product = repo.getProduct(id).stateIn(vmScope, SharingStarted.Lazily, null)
// 事件
val events = eventFlow.shareIn(vmScope, SharingStarted.Eagerly, replay = 0)
6. 常见注意
- scope 销毁时收集会取消,转成的热流不再更新
- Lazily 下无订阅者时不运行,首次订阅后才开始
- replay=0 时新订阅者收不到历史,只收订阅后的值
2.7 快速参考表
| 选择标准 | 选择冷流 | 选择热流 |
|---|---|---|
| 数据独立性 | 每消费者独立数据 | 多消费者共享 |
| 资源消耗 | 敏感、需及时释放 | 可共享、避免重复 |
| 数据实时性 | 每次需最新计算 | 持续更新 |
| 生命周期 | 精确控制开始/结束 | 生产独立于消费者 |
| 使用场景 | 网络、DB、一次性 | UI 状态、事件、实时 |
| 典型实现 | flow{}、flowOf()、asFlow() | StateFlow、SharedFlow |
| 转换方法 | - | shareIn()、stateIn() |
一句话:要独立、按需、省资源 → 冷流;要共享、实时、多订阅 → 热流。
第三部分:Flow 核心操作
第3章:Flow(冷流)- 基础构建与消费
3.1 构建器
| 构建器 | 适用场景 | 执行方式 |
|---|---|---|
flow{} | 自定义生产逻辑 | 惰性执行,collect 时才开始 |
flowOf() | 固定值序列 | 轻量,适合静态数据 |
asFlow() | 集合转流 | 依赖原集合 |
channelFlow{} | 多协程并发发送、高吞吐 | 立即执行,支持 launch + send |
callbackFlow{} | 回调 API 转流 | 见 3.3 节 |
flow { emit(1); emit(2) }
flowOf(1, 2, 3)
listOf(1, 2, 3).asFlow()
channelFlow {
launch { send(1) }
launch { send(2) }
}
构建器选择:单协程顺序生产 → flow;多协程并发 → channelFlow;回调转流 → callbackFlow。
channelFlow 说明:内部用 Channel,可用 launch 并发 send,适合多协程并发生产。与 flow 不同,flow 内 emit 是顺序挂起的,不能在不同协程里并发 emit。
3.2 消费:collect 家族
| 方法 | 作用 | 适用 |
|---|---|---|
collect | 挂起收集,对每个值依次执行 | 通用消费 |
collectLatest | 新值到来取消上一处理,只处理最新 | 搜索建议、防抖后请求 |
launchIn(scope) | 在 scope 中启动收集,不阻塞 | ViewModel/Fragment「启动即忘」 |
flowOf(1, 2, 3).collect { println(it) }
searchQueryFlow.debounce(300).collectLatest { fetchSuggestions(it) }
dataFlow.onEach { updateUI(it) }.launchIn(viewModelScope)
自定义 Collector:collect 可传入实现 FlowCollector<T> 的对象,用于批量处理等自定义逻辑。
步骤:
- 实现
FlowCollector<T>,重写emit - 在
emit中决定何时处理、如何聚合(如攒够一批再处理) - 将对象传给
collect
flow.collect(object : FlowCollector<Int> {
val batch = mutableListOf<Int>()
override suspend fun emit(value: Int) {
batch.add(value) // 1. 收集到缓冲区
if (batch.size >= 10) { // 2. 满一批时处理
processBatch(batch.toList())
batch.clear()
}
}
}) // 3. 流发出的每个值会调用 emit
3.3 callbackFlow:回调转流
将基于回调的 API 转为 Flow。trySend 发数据,close() / close(Throwable) 手动关闭(完成或出错时),awaitClose 在流关闭或收集取消后执行清理(如取消注册、释放资源)。
callbackFlow 执行流程:
开始 → 1.创建回调(trySend/close) → 2.注册 API → 3.awaitClose 挂起
│
close()或收集取消
│
▼
执行 awaitClose 块(清理)
fun fetchUserFlow(userId: String): Flow<User> = callbackFlow {
val callback = object : ApiCallback<User> {
override fun onSuccess(data: User) { trySend(data); close() }
override fun onError(e: Throwable) { close(e) }
}
api.getUser(userId, callback)
awaitClose { api.cancel(userId) }
}
3.4 错误处理
| 操作符 | 作用 | 链中顺序 |
|---|---|---|
| retry(N) | 异常时按条件重试,最多 N 次 | 在 catch 之前 |
| catch | 捕获上游异常,可 emit 恢复 | 在 retry 之后 |
| onCompletion | 流结束时回调,类似 finally | 任意位置 |
推荐顺序:retry → catch → onCompletion → collect
错误处理流程图:
流执行
│
▼
┌─────────────┐
│ 上游 emit │
└─────────────┘
│ 异常?
├──是──→ retry 重试?──是──→ 回到「上游 emit」
│ │
│ └──否──→ catch 捕获?──emit 恢复值──→ 继续
│ │
│ └──不处理──→ 异常向上传播
│
└──否──→ 继续
│
▼
流结束 ──→ onCompletion(无论成功/失败都执行)
flow { emit(1); throw RuntimeException() }
.catch { emit(-1) }
.collect { println(it) } // 1, -1
flow { /* 可能失败 */ }
.retry(2) { it is IOException }
.catch { emit("默认") }
.collect { println(it) }
flow { emit(1); emit(2) }
.onCompletion { cause -> println(if (cause == null) "正常" else "异常") }
.collect { println(it) }
第4章:Flow 操作符全景概览
操作符分类总图:
转换: map, transform, scan, mapLatest ── 1→1 或 1→N
过滤: filter, take, drop, distinctUntilChanged, debounce, sample
组合: zip, combine, flatMapConcat/Merge/Latest
调度: flowOn, withContext
背压: buffer, conflate, collectLatest
冷转热: shareIn, stateIn
错误: catch, retry, onCompletion
| 操作符 | 作用 | 输入→输出 | 适用场景 |
|---|---|---|---|
| map | 一对一转换 | 1 个值 → 1 个值 | 简单类型转换、属性提取 |
| transform | 灵活转换 | 1 个值 → 0~N 个值 | 条件发射、展开数据 |
| scan | 累积计算 | 流 → 所有中间结果 | 实时统计、进度计算 |
| mapLatest | 转换最新值 | 最新值 → 转换结果 | 取消过时的异步转换 |
// map:每个值乘 2,结果 2, 4, 6
flowOf(1, 2, 3).map { it * 2 }
// transform:每个值发射原值+平方,结果 1, 1, 2, 4
flowOf(1, 2).transform {
emit(it)
emit(it * it)
}
// scan:累积求和,发射每步中间结果,结果 0, 1, 3, 6
flowOf(1, 2, 3).scan(0) { acc, v -> acc + v }
// mapLatest:新查询到来时取消旧请求,只保留最新结果
queryFlow.mapLatest { api.search(it) }
转换操作符选择:1→1 类型转换 → map;1→0~N 展开 → transform;累积中间结果 → scan;新值取消旧转换 → mapLatest。
4.2 过滤系操作符
| 操作符 | 作用 | 效果 | 适用场景 |
|---|---|---|---|
| filter | 条件过滤 | 保留符合条件的值 | 数据筛选 |
| take | 取前 N 个 | 只取前 N 个后完成 | 限制数据量 |
| drop | 跳过前 N 个 | 跳过前 N 个值 | 忽略初始数据 |
| distinctUntilChanged | 去重 | 过滤连续重复 | UI 状态更新 |
| debounce | 防抖 | 稳定期后取最后值 | 搜索框输入 |
| sample | 采样 | 定期取最新值 | 高频数据降频 |
filter:保留符合条件的值。
flowOf(1, 2, 3, 4).filter { it % 2 == 0 } // 2, 4
take / drop:取前 N 个或跳过前 N 个。
flowOf(1, 2, 3, 4).take(2) // 1, 2
flowOf(1, 2, 3, 4).drop(2) // 3, 4
distinctUntilChanged:过滤连续重复。
flowOf(1, 1, 2, 2, 1).distinctUntilChanged() // 1, 2, 1
debounce / sample:时间相关,防抖取最后、采样取最新。
inputFlow.debounce(300) // 搜索防抖(ms)
sensorFlow.sample(1000) // 高频降频(ms)
过滤操作符选择:按条件筛 → filter;限制数量 → take/drop;去连续重 → distinctUntilChanged;输入防抖 → debounce;高频降采样 → sample。
4.3 组合系操作符
| 操作符 | 作用 | 组合方式 | 适用场景 |
|---|---|---|---|
| zip | 一对一配对 | 两值都到才组合 | 合并相关数据 |
| combine | 动态组合 | 任一更新即重算 | UI 多状态合并 |
| flatMapConcat | 顺序展开 | 一个完成再下一个 | 保证顺序 |
| flatMapMerge | 并发展开 | 多路并发(默认 16) | 提高吞吐 |
| flatMapLatest | 最新值展开 | 新值取消旧展开 | 搜索建议 |
zip:两流一一配对,等待两边都到。
flowOf("A", "B").zip(flowOf(1, 2)) { a, b -> "$a$b" } // A1, B2
combine:任一更新就重新组合,适合多输入表单、筛选。
queryFlow.combine(filterFlow) { q, f -> SearchParams(q, f) }
zip vs combine:zip 需两边都到才配对,适合等长合并;combine 任一更新即重算,适合多状态联动。
flatMap 三兄弟对比图:
输入: 1, 2, 3
flatMapConcat(顺序):
1→流₁→等流₁完→2→流₂→等流₂完→3→流₃
结果顺序: 流₁所有值, 流₂所有值, 流₃所有值
flatMapMerge(并发):
1→流₁ \
2→流₂ ─→ 同时执行(最多N个),结果交错
3→流₃ /
flatMapLatest(最新):
1→流₁──→2 到来,取消流₁→2→流₂──→3 到来,取消流₂→3→流₃
结果: 只保留流₃的值
flowOf(1,2,3).flatMapConcat { fetch(it).asFlow() } // 顺序
flowOf(1,2,3).flatMapMerge(2) { fetch(it).asFlow() } // 最多 2 并发
queryFlow.flatMapLatest { flow { emit(api.search(it)) } } // 只保留最新
flatMap 选择:保证顺序 → Concat;提高吞吐 → Merge;只保留最新(搜索)→ Latest。
4.4 线程调度
| 操作符/函数 | 作用 | 适用场景 |
|---|---|---|
| flowOn | 指定其上游的执行上下文(不影响下游) | 网络请求、数据库查询等 IO 操作 |
| withContext | 临时切换上下文 | collect 内切主线程更新 UI |
// flowOn:上游在 IO 线程执行,收集在调用方上下文
flow { emit(fetch()) }.flowOn(Dispatchers.IO)
// withContext:collect 内临时切主线程更新 UI
dataFlow.collect { data ->
withContext(Dispatchers.Main) { updateUI(data) }
}
4.5 背压处理
背压:生产速度 > 消费速度时的数据积压。不处理可能内存暴涨或阻塞。
一、冷流 vs 热流
| 对比项 | 冷流(Flow) | 热流(SharedFlow) |
|---|---|---|
| 配置方式 | 操作符 buffer()、conflate() | 构造参数 extraBufferCapacity、onBufferOverflow |
| 默认缓冲 | 无 buffer 则无缓冲 | replay=0、extraBufferCapacity=0 → 总容量 0 |
| 默认表现 | emit 挂起等 collect 取走 | emit 挂起等收集者取走 |
| 相同点 | onBufferOverflow 语义一致;collectLatest 均可用于收集 |
结论:默认都是无缓冲,emit 挂起等消费。不丢数据、不堆积,生产被消费拖慢。需要缓冲时:冷流加 buffer(),热流设 extraBufferCapacity 或 replay。
默认参数:冷流 buffer() 默认 capacity=64、SUSPEND;热流 SharedFlow 默认 replay=0、extraBufferCapacity=0、SUSPEND。使用 DROP_* 策略时需 extraBufferCapacity ≥ 1 才有实际效果。
二、冷流背压
emit ──→ 用了 buffer?──否──→ 挂起等消费 ──→ 消费端
│ └──是──→ 入队 ──→ 队列满?──是──→ onBufferOverflow
│ └──否──→ 消费端
| 操作符 | 说明 | 示例 |
|---|---|---|
buffer() | 64,SUSPEND,不丢 | flow.buffer().collect { } |
conflate() | 容量 1,DROP_OLDEST,只留最新 | sensorFlow.conflate().collect { } |
buffer(N, DROP_OLDEST) | 同 conflate,容量可调 | flow.buffer(16, BufferOverflow.DROP_OLDEST) |
collectLatest { } | 消费端:新值到来取消当前 | queryFlow.collectLatest { api.search(it) } |
flow { emit(1); emit(2) }.buffer().collect { slowProcess(it) } // 不丢数据
sensorFlow.conflate().collect { updateUI(it) } // 只要最新
queryFlow.collectLatest { api.search(it) } // 消费端只处理最新
三、热流背压
emit/tryEmit ──→ 入缓冲(replay+extraBufferCapacity) ──→ 满?──是──→ onBufferOverflow
└──否──→ 多 collect 取
extraBufferCapacity:额外缓冲;onBufferOverflow:满时策略(SUSPEND / DROP_OLDEST / DROP_LATEST)。
MutableSharedFlow<Int>() // 默认无额外缓冲
MutableSharedFlow<Int>(extraBufferCapacity = 64) // 提高吞吐
MutableSharedFlow<Int>(replay = 1, extraBufferCapacity = 16, onBufferOverflow = BufferOverflow.DROP_OLDEST) // 只要最新
四、onBufferOverflow(冷热通用)
| 策略 | 行为 |
|---|---|
| SUSPEND | 挂起等消费,不丢 |
| DROP_OLDEST | 丢最旧,放最新 |
| DROP_LATEST | 丢最新,保留队列 |
数据流:buffer → [生产]─[队列]─[消费];conflate → [生产]─[最新]─[消费];collectLatest → 新值到来取消当前。
五、速查
| 需求 | 冷流 | 热流 |
|---|---|---|
| 不丢数据 | buffer() 或默认 | extraBufferCapacity 足够 + SUSPEND |
| 只要最新 | conflate() | extraBufferCapacity + DROP_OLDEST |
| 消费端只处理最新 | collectLatest { } | collectLatest { } |
组合:buffer 与 conflate 二选一;collectLatest 可与任一边搭配。
4.6 批量数据处理
注意:Kotlin 1.9+ 的 kotlinx-coroutines 提供了
chunked(size)操作符(标记为@ExperimentalCoroutinesApi),用于按大小分批。若需时间窗口分批,需自定义实现。
// 方式一:chunked(需 @OptIn(ExperimentalCoroutinesApi::class))
@OptIn(ExperimentalCoroutinesApi::class)
dataFlow
.chunked(50)
.collect { batch ->
database.insertBatch(batch)
}
// 方式二:buffer + collect 内攒批(需定义 batchBuffer,流结束用 onCompletion 处理剩余)
val batchBuffer = mutableListOf<Item>()
dataFlow
.buffer(100)
.onCompletion { if (batchBuffer.isNotEmpty()) database.insertBatch(batchBuffer) }
.collect { item ->
batchBuffer.add(item)
if (batchBuffer.size >= 50) {
database.insertBatch(batchBuffer.toList())
batchBuffer.clear()
}
}
4.7 实战速查:搜索场景(经典组合)
searchQueryFlow
.debounce(300) // 防抖
.filter { it.length >= 3 }
.distinctUntilChanged()
.flatMapLatest { flow { emit(api.search(it)) } } // search 返回 List,整份发射
.flowOn(Dispatchers.IO) // 若 search 为 IO,上游切 IO
.catch { emit(emptyList()) }
4.8 操作符组合黄金法则
- 顺序很重要:操作符按声明顺序执行,flowOn 只影响其上游
- 尽早过滤:先用 filter 减少后续数据量
- 合理防抖:用户输入用 debounce,实时数据用 sample
- 线程优化:IO 操作用
flowOn(Dispatchers.IO) - 错误恢复:用 catch 在适当位置恢复,retry 在 catch 前
- 资源清理:无限流要确保能被取消,callbackFlow 务必 awaitClose
典型顺序:filter/map → debounce → flatMapLatest → flowOn(IO) → catch → collect。flowOn 影响其上游,故放靠近耗时操作前;搜索场景(4.7)将 flowOn 置于 flatMapLatest 之后,使 api.search 在 IO 执行。
操作符链数据流程图(与典型顺序一致,flowOn 影响其上游):
生产者(flow)
│
▼
filter/map ──── 过滤、转换
│
▼
debounce ──── 防抖(用户输入场景)
│
▼
flatMapLatest ──── 展开,新值取消旧(耗时操作在此)
│
▼
flowOn(IO) ──── 以上游在 IO 执行
│
▼
catch ──── 异常恢复
│
▼
collect ──── 消费,通常主线程
常见问题速查
| 问题 | 建议 |
|---|---|
| 多次 collect 重复请求 | 用 shareIn/stateIn 转热流 |
| UI 不更新 | 检查 copy()、主线程更新 |
| 页面销毁仍在收集 | repeatOnLifecycle、flowWithLifecycle |
| Toast 重复显示 | 用 SharedFlow(replay=0),不用 StateFlow |
| 回调转 Flow | callbackFlow,用 trySend/close、awaitClose |
第四部分:SharedFlow 与 StateFlow
第5章:SharedFlow(热流)- 事件与广播专家
SharedFlow 是热流,生产与消费解耦,多订阅者共享同一数据源。适用于事件总线、实时推送、广播等「一发多收」场景。与 StateFlow 区别:无初始值、replay 可配、无自动去重。
5.1 配置三要素
背压关系:
extraBufferCapacity、onBufferOverflow是背压处理的一环,与冷流buffer(capacity, onBufferOverflow)语义一致。完整背压流程见 4.5 节。
| 参数 | 作用 | 默认值 |
|---|---|---|
| replay | 新订阅者立即可收到的历史值数量 | 0 |
| extraBufferCapacity | 生产快于消费时的额外缓冲(背压容量) | 0 |
| onBufferOverflow | 缓冲区满时的处理策略(背压溢出策略) | SUSPEND |
replay 取值:
0:新订阅者只收订阅后的值,适合事件(Toast、点击)1:新订阅者拿到最新 1 个,适合实时状态N:新订阅者拿到最近 N 个,适合聊天记录等
onBufferOverflow:
| 策略 | 行为 | 适用 |
|---|---|---|
| SUSPEND | 挂起生产者直到有空间 | 重要数据(消息、订单) |
| DROP_OLDEST | 丢最旧,放最新 | 只要最新(传感器、股价) |
| DROP_LATEST | 丢最新,保留队列 | 顺序敏感 |
5.2 参数组合示例
| 场景 | replay | extraBufferCapacity | onBufferOverflow |
|---|---|---|---|
| 事件总线(Toast、点击) | 0 | 0 或小值 | SUSPEND |
| 实时数据(股价、位置) | 1 | 10~50 | DROP_OLDEST |
| 聊天消息 | 50~100 | 50 | SUSPEND |
事件总线 + tryEmit:
extraBufferCapacity=0且无订阅者时,tryEmit返回 false,事件会丢。可设小缓冲或接受「无订阅者时丢失」。
// 事件总线
MutableSharedFlow<Event>(replay = 0, extraBufferCapacity = 0)
// 实时数据
MutableSharedFlow<Price>(replay = 1, extraBufferCapacity = 10, onBufferOverflow = BufferOverflow.DROP_OLDEST)
5.3 发射策略:emit() vs tryEmit()
| 特性 | emit() | tryEmit() |
|---|---|---|
| 执行方式 | 挂起,可能等待 | 非挂起,立即返回 |
| 调用位置 | 必须在协程内 | 任意线程(含回调) |
| 缓冲区满时 | 挂起等待 | 返回 false,数据可能丢 |
emit vs tryEmit 选择流程图:
需要发射数据?
│
├── 在协程内?──否──→ 只能用 tryEmit()
│
└── 在协程内?──是──→ 数据必须送达?
│
├──是──→ emit()
└──否──→ 可丢?──是──→ tryEmit() + DROP_OLDEST
└──否──→ emit()
选择速查:
| 场景 | 推荐 |
|---|---|
| 重要数据、用户操作 | emit() |
| 回调中、非协程环境 | tryEmit() |
| 高频数据、可丢失 | tryEmit() + DROP_OLDEST |
// 协程内:保证送达
viewModelScope.launch { eventFlow.emit(ClickEvent()) }
// 回调内:只能用 tryEmit
button.setOnClickListener {
eventFlow.tryEmit(ClickEvent())
}
5.4 常见陷阱与对策
| 陷阱 | 表现 | 对策 |
|---|---|---|
| replay 过大 | 内存持续增长,新订阅收到大量旧数据 | replay 按需设,用 WhileSubscribed(replayExpirationMillis) 过期清除 |
| 缓冲区满 | emit 长时间挂起,tryEmit 频繁返回 false | 增大 extraBufferCapacity,或改用 DROP_OLDEST |
| 多订阅重复处理 | 同一事件被多个收集者各处理一次 | 事件加 id,收集端用 Set 去重 |
| 生命周期不当 | 页面销毁后仍在收集,内存泄漏或崩溃 | 用 repeatOnLifecycle(STARTED)、flowWithLifecycle |
| 冷流重复请求 | 多处 collect 同一冷流,重复请求网络/DB | 用 shareIn 转热流,多订阅共享 |
| 背压忽略 | 生产远快于消费,内存暴涨 | 设合理缓冲区,或 DROP_OLDEST,消费者用 collectLatest |
| 异常未捕获 | 发射时抛异常导致流终止 | 在 emit 前 try-catch,或上游用 catch 转错误值 |
| 配置变更重复订阅 | 屏幕旋转后 Activity 重建,重复收集 | 在 ViewModel 中收集,或 repeatOnLifecycle 自动处理 |
| 事件与状态混用 | 用 SharedFlow(replay=1) 做事件,丢失或重复 | 事件用 replay=0,状态用 StateFlow |
5.5 SharingStarted 速查
| 策略 | 行为 | 适用场景 |
|---|---|---|
| Eagerly | 立即开始收集上游,永不停止 | 全局配置、主题、语言 |
| Lazily | 首个订阅者出现时开始,之后持续运行 | 页面级数据、按需加载 |
| WhileSubscribed | 有订阅时运行,最后订阅者离开后延迟停止 | 页面切换频繁、省电省资源 |
WhileSubscribed 参数:
| 参数 | 默认 | 含义 |
|---|---|---|
| stopTimeoutMillis | 0 | 最后订阅者离开后,延迟多久停止 |
| replayExpirationMillis | Long.MAX_VALUE | 停止后多久清除 replay 缓存(0=立即) |
SharingStarted.Eagerly
SharingStarted.Lazily
SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000, replayExpirationMillis = 60000)
选择建议:全局且持续 → Eagerly;进页面才要 → Lazily;频繁进出页面 → WhileSubscribed。
第6章:StateFlow(热流)- 新一代状态容器
StateFlow 专为 UI 状态设计,等价于 SharedFlow(replay=1) + 必有初始值 + 自动去重。适合 ViewModel 暴露状态,配合 Compose 或 View 使用。跨平台,可替代 LiveData。
6.1 核心特性
| 特性 | 说明 | 示例 |
|---|---|---|
| 必有初始值 | 新订阅者立即可见,无需空判断 | MutableStateFlow("") |
| 自动去重 | equals() 相同不发射,减少无效重组 | 连续 value = 1 只触发一次 |
| 固定 replay=1 | 新订阅者立即拿到当前值 | 后加入的订阅者先收当前状态 |
| 线程安全 | 支持多线程更新 | IO 线程 value = x 安全 |
val state = MutableStateFlow(0)
state.value = 1
state.value = 1 // 相同,不触发
state.value = 2 // 不同,触发
// 新订阅者立即可见(collect 挂起等待后续更新,会先收到当前值 2)
state.collect { println(it) } // 先打印 2,再等待后续
6.2 原子更新:update() / value
直接赋值:简单场景用 state.value = newValue。
原子读写:并发场景用 update { },保证 read-modify-write 原子性。
// 简单更新
_state.value = UserState(loading = false, data = user)
// 原子更新(推荐)
_state.update { it.copy(name = newName, age = newAge) }
// 条件更新
_state.update { if (it.count > 0) it.copy(count = it.count - 1) else it }
注意:update 的 lambda 可能重试,逻辑需幂等。
6.3 从 LiveData 迁移到 StateFlow
ViewModel:
// Before
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
// After
private val _state = MutableStateFlow<UserState>(UserState())
val state: StateFlow<UserState> = _state.asStateFlow()
状态更新:用 copy() 创建新对象,一次赋值。
_state.value = _state.value.copy(loading = true, error = null)
_state.value = _state.value.copy(loading = false, user = result)
UI 层(View):
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.state.collect { updateUI(it) }
}
}
UI 层(Compose):
val state by viewModel.state.collectAsStateWithLifecycle()
6.4 UI 层安全消费
核心问题:直接在 lifecycleScope.launch { flow.collect {} } 时,页面进入后台或旋转重建后,收集可能泄漏或重复。需用生命周期感知的方式,在 STARTED 时收集、STOPPED 时停止。
| 方式 | 形式 | 区别 | 适用 |
|---|---|---|---|
| repeatOnLifecycle | 作用域函数,包裹收集块 | 块内可启动多个 collect,统一受生命周期控制 | Activity/Fragment,需同时收集多个流 |
| flowWithLifecycle | Flow 操作符,返回新 Flow | 对单个流做生命周期过滤,写法更短 | Activity/Fragment,只收集一个流 |
| collectAsStateWithLifecycle | Compose 收集为 State | Compose 专用,自动随生命周期启停,避免重组时重复收集 | Compose |
| SavedStateHandle.getStateFlow | 从 Handle 获取 StateFlow | 用于创建进程死亡后可恢复的流,不是收集方式 | ViewModel 内需持久化的状态 |
生命周期与收集流程图:
Activity/Fragment 生命周期
onCreate → onStart → onResume → onPause → onStop → onDestroy
│ │
repeatOnLifecycle(STARTED) 收集区: │
[======== 收集运行 ========]
[停止收集]
进入 STARTED → 启动 collect
离开 STARTED → 取消 collect,避免泄漏
repeatOnLifecycle:块内协程随生命周期取消,进入 STARTED 时启动,离开时取消。
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch { viewModel.state.collect { updateUI(it) } }
launch { viewModel.events.collect { showToast(it) } }
}
}
flowWithLifecycle:流在生命周期外不发射,等同于「单流版 repeatOnLifecycle」。
lifecycleScope.launch {
viewModel.state
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.collect { updateUI(it) }
}
collectAsStateWithLifecycle:在 Compose 中收集为 State,生命周期感知,避免 LaunchedEffect(Unit) 导致重复收集。
@Composable
fun Screen(viewModel: MyVM) {
val state by viewModel.state.collectAsStateWithLifecycle()
Text(state.toString())
}
SavedStateHandle.getStateFlow:用于状态持久化,与收集方式配合使用。
class VM(handle: SavedStateHandle) : ViewModel() {
val state = handle.getStateFlow("key", defaultValue)
}
6.5 复杂 UI 状态
当界面要展示多种数据(商品、评价、库存等),或状态之间有依赖时,需要用不同方式组织 StateFlow。
1. 状态合并(combine)
场景:商品详情页要同时展示商品信息、评价列表、库存,三者来自不同接口。若各自用 StateFlow,UI 可能先看到商品、再看到评价,出现闪烁。
做法:用 combine 把多个流合成一个,任一变化就重新组合,UI 只订阅一个流,一次拿到完整数据。
val uiState = combine(productFlow, reviewsFlow, stockFlow) { product, reviews, stock ->
ProductDetailState(product, reviews, stock)
}.stateIn(scope, SharingStarted.WhileSubscribed(5000), ProductDetailState())
// UI:uiState.collect { 一次拿到 product、reviews、stock }
2. 状态分片
场景:整个页面用一个超大 StateFlow,主题、用户、通知都在一起。改个通知数字,主题组件也会重组,浪费性能。
做法:按业务拆成多个小 StateFlow,每个组件只订阅自己需要的。主题改只影响主题组件,用户改只影响用户组件。
val themeState = MutableStateFlow(Theme.LIGHT) // 主题组件只 collect 这个
val userState = MutableStateFlow<User?>(null) // 用户区域只 collect 这个
val notificationCount = MutableStateFlow(0) // 通知图标只 collect 这个
3. 派生状态
场景:购物车状态只有 List<Item>,但 UI 还要显示总价、是否为空。每次在 UI 里算一遍,代码重复。
做法:用 map + stateIn 从已有状态算出新状态,ViewModel 统一维护,UI 直接订阅。
val cartState = MutableStateFlow<List<Item>>(emptyList())
val totalPrice = cartState.map { it.sumOf { i -> i.price * i.qty } }
.stateIn(scope, SharingStarted.Eagerly, 0.0)
val isEmpty = cartState.map { it.isEmpty() }
.stateIn(scope, SharingStarted.Eagerly, true)
// UI:totalPrice.collect { 显示总价 };isEmpty.collect { 显示空车提示 }
4. 状态 + 事件分离
场景:Toast、弹窗、导航这类「一次性」反馈,若用 StateFlow 存「当前要显示的 Toast」,新订阅者会重复看到旧的 Toast。
做法:状态(列表、加载中)用 StateFlow;事件(Toast、导航)用 SharedFlow(replay=0),只发给当前订阅者,不重播。
复杂状态管理流程图:
多数据源?──是──→ combine 合并
│
└──否──→ 大状态导致过度重组?──是──→ 状态分片
│
└──否──→ 需从已有状态计算?──是──→ 派生状态(map+stateIn)
│
└──否──→ 有一次性反馈?──是──→ 状态+事件分离
└──否──→ 单一 StateFlow 即可
private val _state = MutableStateFlow(UiState()) // 状态:可重播
private val _events = MutableSharedFlow<UiEvent>() // 事件:一次性
6.6 常见陷阱与对策
| 陷阱 | 表现 | 对策 |
|---|---|---|
| 不用 copy() | 改可变属性,UI 不更新 | 用不可变数据类,更新时 copy() |
| 非主线程更新 UI | 崩溃或闪烁 | 耗时操作用 withContext(IO),结果在主线程赋给 state |
| Compose 错误收集 | 每次重组都 collect,泄漏 | 用 collectAsStateWithLifecycle |
| 状态事件混用 | 事件丢失或重复 | 状态用 StateFlow,事件用 SharedFlow(0) |
| 忽略取消 | 资源泄漏 | collect 内 try-finally 或 ensureActive() |
| 大状态对象 | copy 开销大、重组频繁 | 分片、或 Compose 内 remember 缓存 |
| 密封类未覆盖 | when 漏分支,运行异常 | 补全所有分支 |
| Eagerly 拿到旧缓存 | shareIn/stateIn 立即开始时,上游若有缓存,新订阅者可能收到过时值 | WhileSubscribed(replayExpirationMillis=0) 或 Lazily |
简单示例:
// 1. 不用 copy():可变属性改后 UI 不更新
// ❌ state.value.items.add(item)
// ✅ state.update { it.copy(items = it.items + item) }
// 2. 非主线程更新:IO 线程直接赋值可能崩溃
// ❌ withContext(Dispatchers.IO) { _state.value = fetch() }
// ✅ val r = withContext(Dispatchers.IO) { fetch() }; _state.value = r
// 3. Compose 错误收集:每次重组都 collect 导致泄漏
// ❌ LaunchedEffect(Unit) { vm.state.collect { s = it } }
// ✅ val s by vm.state.collectAsStateWithLifecycle()
// 4. 状态事件混用:用 StateFlow 做一次性事件易丢或重复
// ❌ _toastState = MutableStateFlow<String?>(null)
// ✅ _toastEvents = MutableSharedFlow<String>(replay = 0)
// 5. 忽略取消:collect 内开资源未在取消时释放
// ❌ state.collect { val f = openFile(); use(f) }
// ✅ state.collect { val f = openFile(); try { use(f) } finally { f.close() } }
// 6. 大状态对象:整块 copy 开销大
// ❌ state.update { it.copy(wholeBigList = newList) }
// ✅ 分片:themeState、userState 分开;或 Compose 内 remember(key) { }
// 7. 密封类未覆盖:漏分支导致运行时异常
// ❌ when(s) { is Success -> ... } // 漏 is Error、is Loading
// ✅ when(s) { is Success -> ; is Error -> ; is Loading -> } // 补全所有分支
// 8. Eagerly 拿到旧缓存:立即开始时,上游若有缓存会传给新订阅者
// ❌ shareIn(Eagerly) 且上游有缓存
// ✅ WhileSubscribed(replayExpirationMillis = 0) 或 Lazily
6.7 StateFlow vs SharedFlow 速查
对比:
| 维度 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须有 | 无(replay 决定新订阅是否拿历史) |
| 自动去重 | 有 | 无 |
| replay | 固定 1 | 可配 0、1、N |
| 典型场景 | UI 状态 | 事件、广播、实时数据 |
选择速查:
| 需求 | 选择 | 示例 |
|---|---|---|
| UI 状态(列表、详情、加载) | StateFlow | MutableStateFlow(UiState()) |
| 一次性事件(Toast、Snackbar、导航) | SharedFlow(replay=0) | MutableSharedFlow<UiEvent>() |
| 实时数据(股价、位置) | SharedFlow(replay=1) | MutableSharedFlow<Price>(replay=1) |
| 需最近 N 条(聊天记录) | SharedFlow(replay=N) | MutableSharedFlow<Message>(replay=50) |
StateFlow vs SharedFlow 决策流程图:
需要热流?
│
├──否──→ 用冷流 Flow
│
└──是──→ 需要初始值 + 自动去重?
│
├──是──→ StateFlow(UI 状态)
│
└──否──→ replay=0?──是──→ SharedFlow(0)(事件)
│
└──否──→ SharedFlow(1或N)(实时/历史)
混合使用:ViewModel 内常见「状态 + 事件」模式。
private val _state = MutableStateFlow(UiState())
val state: StateFlow<UiState> = _state.asStateFlow()
private val _events = MutableSharedFlow<UiEvent>()
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
附录 A:四大数据流终极对比表
整体对比图:
Flow StateFlow SharedFlow LiveData
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
温度 │ 冷流 │ │ 热流 │ │ 热流 │ │ 热流 │
初始值 │ 无 │ │ 必须有 │ │ 可选 │ │ 可选 │
去重 │ 无 │ │ 有 │ │ 无 │ │ 无 │
场景 │ 请求/查询│ │ UI 状态 │ │ 事件/实时│ │Android UI│
└─────────┘ └─────────┘ └─────────┘ └─────────┘
基础属性:
| 维度 | Flow | StateFlow | SharedFlow | LiveData |
|---|---|---|---|---|
| 温度 | 冷流 | 热流 | 热流 | 热流 |
| 数据共享 | 每订阅独立 | 多订阅共享 | 多订阅共享 | 多观察者共享 |
| 初始值 | 无 | 必须有 | 可选(replay) | 可选 |
| 生命周期 | 依赖收集者 | 独立,需配合 Lifecycle | 独立,需配合 Lifecycle | 内置感知 |
重播与去重:
| 维度 | Flow | StateFlow | SharedFlow | LiveData |
|---|---|---|---|---|
| replay/重播 | 无 | 固定 1 | 可配 0/1/N | 无标准 |
| 自动去重 | 无 | 有(equals) | 无 | 无 |
发射与消费:
| 维度 | Flow | StateFlow | SharedFlow | LiveData |
|---|---|---|---|---|
| 发射方式 | emit(构建器内) | value= / update() | emit() / tryEmit() | postValue / setValue |
| 消费方式 | collect | collect | collect | observe |
| 是否挂起 | collect 挂起 | collect 挂起 | collect 挂起 | 否 |
背压与线程:
| 维度 | Flow | StateFlow | SharedFlow | LiveData |
|---|---|---|---|---|
| 背压处理 | 内置挂起 | DROP_OLDEST 语义 | 可配 SUSPEND/DROP_* | 无 |
| 线程安全 | 依赖 flowOn | 高并发安全 | 高并发安全 | 主线程安全 |
平台与生态:
| 维度 | Flow | StateFlow | SharedFlow | LiveData |
|---|---|---|---|---|
| 平台 | Kotlin 多平台 | Kotlin 多平台 | Kotlin 多平台 | 仅 Android |
| 协程集成 | 深度 | 深度 | 深度 | 有限 |
| 操作符 | 丰富 | 继承 Flow | 继承 Flow | 少 |
适用场景:
| 维度 | Flow | StateFlow | SharedFlow | LiveData |
|---|---|---|---|---|
| 典型场景 | 网络请求、DB 查询、一次性 | UI 状态、应用状态 | 事件总线、实时推送、广播 | Android UI(传统) |
| 冷转热 | - | stateIn | shareIn | asLiveData |
内存与性能:
| 维度 | Flow | StateFlow | SharedFlow | LiveData |
|---|---|---|---|---|
| 内存特点 | 按需执行,无订阅无消耗 | 常驻最新 1 值 | 可配缓冲,replay 影响 | 中等 |
| 多订阅开销 | 每订阅独立生产 | 共享 1 份 | 共享 1 份 | 共享 1 份 |
| 测试便利性 | 易于单测 | 易测 | 易测 | 易测 |
创建方式:
| 类型 | 创建方式 |
|---|---|
| Flow | flow { }、flowOf()、asFlow() |
| StateFlow | MutableStateFlow(init)、冷流 .stateIn() |
| SharedFlow | MutableSharedFlow()、冷流 .shareIn() |
| LiveData | MutableLiveData()、Transformations |
选择口诀:请求/查询用 Flow,UI 状态用 StateFlow,事件用 SharedFlow(0),实时共享用 SharedFlow(1)。
全文核心速查
选型总流程图:
数据需求?
│
├── 请求/查询(每次独立)──→ 冷流 Flow(若需多订阅共享则加 shareIn/stateIn)
│
├── UI 状态(必有当前值)──→ StateFlow
│
├── 一次性事件(Toast、导航)──→ SharedFlow(replay=0)
│
├── 实时共享(多订阅、要最新)──→ SharedFlow(replay=1) 或 shareIn
│
└── 其他/组合场景 ──→ 参考 2.5 典型场景表
| 场景 | 选型 | 关键配置 |
|---|---|---|
| 网络/DB 请求 | Flow(冷流) | flowOn(IO) + catch |
| UI 状态 | StateFlow | 必有初始值,copy() 更新 |
| Toast/导航等事件 | SharedFlow(replay=0) | tryEmit 或 emit |
| 实时数据共享 | SharedFlow(replay=1) | shareIn(WhileSubscribed) |
| 多订阅共享冷流 | shareIn / stateIn | 选 SharingStarted |
| 搜索防抖 | debounce + flatMapLatest | 300ms debounce |
| 生命周期收集 | repeatOnLifecycle | STARTED 时收集 |
| 回调转流 | callbackFlow | trySend + close + awaitClose |