Kotlin Flow 从冷到热:一篇撑起面试的流式编程指南
Flow 是 Kotlin 协程生态中处理异步数据流的核心组件。很多开发者用它写过几个接口请求,但一聊到冷流热流、背压、操作符,就开始含糊不清。
这篇文章从冷流 → 热流 → 操作符 → 背压 → LiveData 对比一条线串下来,帮你建立完整的认知地图。
一、冷流(Cold Flow):现做现卖
定义
冷流 = 每次 collect 都从头执行生产代码。没有消费者,生产代码就不运行。
kotlin
val coldFlow: Flow<Int> = flow {
println("👷 开始生产数据")
for (i in 1..3) {
delay(100) // 模拟耗时
println("→ 产出: $i")
emit(i)
}
}
// 消费者 A
coldFlow.collect { println(" A 消费: $it") }
// 消费者 B(从头再执行一次!)
coldFlow.collect { println(" B 消费: $it") }
输出:
less
👷 开始生产数据
→ 产出: 1 A 消费: 1
→ 产出: 2 A 消费: 2
→ 产出: 3 A 消费: 3
👷 开始生产数据 ← 又重新生产了一遍
→ 产出: 1 B 消费: 1
→ 产出: 2 B 消费: 2
→ 产出: 3 B 消费: 3
每个 collector 拿到的都是完整的一份独立数据。
典型场景
| 场景 | 理由 |
|---|---|
网络请求 flow { emit(retrofit.fetch()) } |
每次 collect 是独立请求 |
| Room DAO 查询 | 每次查询独立游标 |
| 文件逐行读取 | 流式处理,用完即弃 |
冷流创建方式
kotlin
// 1. flow builder(最常用)
flow { for (i in list) emit(i) }
// 2. flowOf
flowOf(1, 2, 3)
// 3. asFlow
listOf(1, 2, 3).asFlow()
(1..3).asFlow()
// 4. callbackFlow(回调转 Flow)
callbackFlow {
api.registerCallback(object : Callback {
override fun onData(data: Data) {
trySend(data)
}
override fun onError(e: Throwable) {
close(e)
}
})
awaitClose { api.unregisterCallback() }
}
二、热流(Hot Flow):一直生产,各自消费
热流 = 生产者与消费者解耦。数据独立产生,多个消费者共享同一份数据流。
2.1 StateFlow --- 有状态的"电视"
始终持有最新值,新订阅者立刻拿到当前状态。
kotlin
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count.asStateFlow()
fun increment() {
_count.update { it + 1 } // 线程安全,原子操作
}
}
// 使用
scope.launch {
vm.count.collect { println(it) } // 立刻打印 0(初始值)
}
vm.increment() // StateFlow 打印 1
StateFlow 本质 = replay = 1 的 SharedFlow + value 属性。
kotlin
// 两段代码等价
MutableStateFlow(0)
MutableSharedFlow<Int>(replay = 1).apply {
tryEmit(0) // 初始值
}
2.2 SharedFlow --- 事件广播
没有初始值,可配置 replay 缓存。
kotlin
private val _toast = MutableSharedFlow<String>()
val toast: SharedFlow<String> = _toast.asSharedFlow()
// 一次性事件
scope.launch {
toast.collect { msg -> showSnackbar(msg) }
}
// 发射
scope.launch {
_toast.emit("登录成功") // 可能挂起
_toast.tryEmit("错误") // 不挂起
}
三、SharedFlow 三个核心参数详解
kotlin
fun <T> MutableSharedFlow(
replay: Int = 0,
extraBufferCapacity: Int = 0,
onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>
3.1 replay --- 重放给新订阅者
新 collector 加入时,先发送最近 N 条历史数据。
kotlin
val rf = MutableSharedFlow<Int>(replay = 2)
rf.tryEmit(1); rf.tryEmit(2); rf.tryEmit(3)
// replay cache = [2, 3]
rf.collect { print("$it ") } // 输出: 2 3
rf.tryEmit(4) // 输出: 4,缓存变 [3, 4]
| replay | 行为 |
|---|---|
| 0 (默认) | 新订阅者等后续数据,没有历史 |
| 1 | 类似 StateFlow,新订阅者拿最新值 |
| N | 拿到最近 N 条 |
3.2 extraBufferCapacity --- 额外缓冲
真正可用的 buffer 总大小 = replay + extraBufferCapacity。
kotlin
val flow = MutableSharedFlow<Int>(
replay = 1,
extraBufferCapacity = 2
)
// 总 buffer = 3(1 个 replay + 2 个额外)
replay 区 vs 额外区的区别:
| replay 区 | 额外区 | |
|---|---|---|
| 用途 | 给新订阅者 | 暂存消费者来不及处理的数据 |
| 溢出丢弃 | ❌ 永不 | ✅ 取决于策略 |
| 清除方式 | resetReplayCache() |
被消费即移除 |
实际上 buffer 内部两个区是独立的:发射 → 先填 replay 区 → 投递所有订阅者 → 有订阅者处理慢 → 进额外区 → 额外区满 → 触发溢出策略。
3.3 onBufferOverflow --- 溢出策略
kotlin
enum class BufferOverflow {
SUSPEND, // 挂起 emit(默认)
DROP_OLDEST, // 丢最老的
DROP_LATEST // 丢最新的
}
实战演示: 假设 replay=0, extraBufferCapacity=1,消费者处理很慢:
kotlin
val flow = MutableSharedFlow<Int>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
scope.launch {
flow.collect { delay(100); println(it) }
}
scope.launch {
repeat(5) { flow.emit(it) }
}
// DROP_OLDEST 下,消费者会跳过一些中间值,只拿到部分数据
选择指南:
| 场景 | 策略 |
|---|---|
| 价格/传感器(最新比老的重要) | DROP_OLDEST |
| 日志/埋点(不能丢) | SUSPEND + 足够 buffer |
| 动画帧(丢掉跟不上帧率的) | DROP_LATEST |
| UI 事件通知(丢弃旧的合理) | DROP_OLDEST |
tryEmit 在 SUSPEND 模式下: buffer 满时返回 false,数据不会发射。
四、冷转热:stateIn / shareIn
kotlin
// 冷流转 StateFlow
val state = someColdFlow
.stateIn(scope, SharingStarted.WhileSubscribed(5000), initialValue)
// 冷流转 SharedFlow
val shared = someColdFlow
.shareIn(scope, SharingStarted.WhileSubscribed(5000), replay = 1)
SharingStarted 三种策略:
| 策略 | 行为 | 适用 |
|---|---|---|
Eagerly |
立刻开始生产,不管有没有订阅者 | 共享数据源头 |
Lazily |
第一个订阅者来了再启动 | 懒加载 |
WhileSubscribed(stopMs) |
订阅者全走后等 stopMs 再停止 | ViewModel + 配置变更(避免频繁重启上游) |
五、核心操作符(Operators)
按功能分类,常用的标记 ⭐:
5.1 变换操作
| 操作符 | 作用 | 示例 |
|---|---|---|
map ⭐ |
一对一变换 | .map { it * 2 } |
flatMapConcat ⭐ |
顺序拍平,串行 | 先请求 A,再请求 B |
flatMapMerge |
并发拍平,顺序聚合 | 同时请求多个接口 |
flatMapLatest ⭐ |
新流取代旧流,取消上一个 | 搜索关键词变化自动取消旧请求 |
transform |
通用变换,可 emit 0 到多次 |
kotlin
// flatMapLatest --- 搜索防抖 + 取消
queryFlow
.debounce(300)
.flatMapLatest { query ->
api.search(query) // 新的 search 开始时,旧的自动取消
}
5.2 过滤操作
| 操作符 | 作用 |
|---|---|
filter ⭐ |
按条件过滤 |
filterNotNull |
去 null |
filterIsInstance<T> |
按类型过滤 |
take |
只取前 N 个 |
drop |
跳过前 N 个 |
debounce ⭐ |
防抖,安静 N ms 后才发射 |
distinctUntilChanged ⭐ |
去重(连续相同值不发射) |
kotlin
flowOf(1, 1, 2, 2, 3)
.distinctUntilChanged()
.collect { print("$it ") } // 1 2 3
5.3 合并操作
| 操作符 | 作用 | 区别 |
|---|---|---|
zip ⭐ |
配对合并,像拉链 | 两个流一对一配对,等最慢的 |
combine ⭐ |
最新值合并 | 任一数据变化就合并所有当前最新值 |
merge |
无脑合并 | 像两股水流汇合 |
kotlin
// combine --- 两个状态直接关联
val nameFlow = MutableStateFlow("")
val ageFlow = MutableStateFlow(0)
combine(nameFlow, ageFlow) { name, age ->
"姓名: $name, 年龄: $age"
}
5.4 收集与副作用
| 操作符 | 作用 |
|---|---|
collect ⭐ |
终端操作,开始消费 |
onEach ⭐ |
每次 emit 前执行(日志、埋点) |
onStart |
收集开始时执行 |
onCompletion ⭐ |
流结束或异常时执行 |
catch ⭐ |
捕获上游异常 |
retry / retryWhen |
失败重试 |
launchIn ⭐ |
在指定 scope 中启动收集 |
5.5 Flow 上下文与背压
kotlin
// flowOn --- 指定上游运行在哪个协程上下文
flow { emit(fetch()) }
.flowOn(Dispatchers.IO) // 上游在 IO 线程
.collect { updateUI(it) } // 下游在主线程
// buffer --- 手动控制缓冲,缓解背压
flowOf(1, 2, 3)
.buffer(capacity = 64) // 显式加 buffer
.map { heavyComputation(it) } // 生产和消费可以并行
.collect { ... }
// conflate --- 丢弃中间值,只保留最新
flowOf(1, 2, 3)
.conflate() // 消费者慢时只拿最新的
.collect { delay(100); println(it) } // 可能 1 和 3,跳过 2
六、背压(Backpressure)
什么是背压?
生产者太快、消费者太慢,数据堆积 → 背压。
Flow 的背压策略
| 策略 | 实现方式 | 说明 |
|---|---|---|
| 挂起(suspend) | 默认行为 | flow { emit() } 挂起等待消费者 |
| 缓冲 | .buffer(N) |
给消费者一个队列,避免频繁挂起 |
| 丢弃 | .conflate() |
只保留最新值,消费者来不及处理的老值丢掉 |
| 合并 | .conflate() / BufferOverflow.DROP_OLDEST |
跟丢弃类似,以最新为准 |
| 响应式拉取 | 默认 Flow 机制 | 天然 pull-based,collect 一个取一个 |
kotlin
// 背压对比
val fastFlow = flow {
repeat(100) {
println("🚀 生产: $it")
emit(it)
}
}
// 方案 1:无缓冲 ------ 生产和消费交替,节奏由消费者决定
fastFlow.collect { value ->
delay(100) // 消费者慢
println("🐢 消费: $value")
}
// 输出:生产 0 → 消费 0 → 生产 1 → 消费 1 ...(一来一回)
// 方案 2:加 buffer ------ 生产可以超前消费 N 个
fastFlow.buffer(10).collect { value ->
delay(100)
println("🐢 消费: $value")
}
// 输出:一口气生产到 10,然后消费 0,生产 11,消费 1 ...
// 方案 3:conflate ------ 消费者只拿最新的
fastFlow.conflate().collect { value ->
delay(100)
println("🐢 消费: $value")
}
// 输出:消费 0,消费 11,消费 22...(跳过中间值)
冷流天然解决背压 :flow { emit() } 是 suspend 函数,消费者不 collect,emit 就挂起------生产者等着消费者,不需要额外机制。
热流的背压更棘手:因为生产者和消费者解耦了,必须靠 buffer + 溢出策略。
选择指南
arduino
消费者能不能跟上生产者?
├── 能 → 不用额外处理
├── 跟不上但数据不重要(传感器、价格)
│ └── conflate / DROP_OLDEST
├── 跟不上但数据必须处理完
│ └── 加大 buffer / 切换到 Channel
└── 跟不上且需要控制丢弃策略
└── 自定义 SharedFlow 参数
七、Flow vs LiveData
对比总表
| 维度 | Flow | LiveData |
|---|---|---|
| 协程原生 | ✅ 完全原生 | ❌ 需要额外封装 |
| 冷/热 | 冷流为主,可转热 | 始终热流 |
| 线程切换 | flowOn 灵活指定 |
postValue 自动切主线程 |
| 操作符 | 丰富(map/flatMap/combine...) | 几乎没有(2.4+ 有 map/zip,有限) |
| 背压 | 原生支持(buffer/conflate) | ❌ 不支持(setValue 覆盖) |
| 生命周期感知 | ❌ 需 repeatOnLifecycle |
✅ 内建 |
| Compose 集成 | collectAsState() |
✅ 直接支持 |
| Room | 原生返回 Flow | 支持(转 Flow) |
| 错误处理 | catch / retry |
无原生方案 |
| 测试 | 纯 Kotlin,易懂 | 需要 InstantTaskExecutorRule |
| 组合多个流 | combine, zip 一把梭 |
MediatorLiveData 手动管理 |
代码对比
kotlin
// ===== LiveData =====
class ViewModel : ViewModel() {
private val _user = MutableLiveData<User?>()
val user: LiveData<User?> = _user
fun loadUser(id: String) {
viewModelScope.launch {
val result = api.getUser(id)
_user.value = result
}
}
}
// 问题:组合多个源需要 MediatorLiveData
private val _full = MediatorLiveData<String>().apply {
addSource(nameLiveData) { name ->
value = combine(ageLiveData.value, name)
}
addSource(ageLiveData) { age ->
value = combine(age, nameLiveData.value)
}
}
// ===== Flow =====
class ViewModel : ViewModel() {
private val _userId = MutableStateFlow("")
val user: StateFlow<UserUiState> = _userId
.debounce(300)
.flatMapLatest { api.getUser(it) }
.map { it.toUiState() }
.catch { emit(UserUiState.Error(it)) }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), UserUiState.Loading)
}
// 组合多个 Flow
combine(nameFlow, ageFlow, avatarFlow) { name, age, avatar ->
UserProfile(name, age, avatar)
}
什么时候还是可以用 LiveData?
kotlin
// Activity / Fragment 里最简单的观察
model.user.observe(this) { user ->
binding.name.text = user.name
}
// 一行搞定,不需要 lifecycleScope.repeatOnLifecycle
对比下来 observe 确实比 repeatOnLifecycle + collect 简洁,但 Flow 可以用扩展函数抹平差异:
kotlin
// 封装一次,到处用
fun <T> Flow<T>.observeIn(lifecycle: Lifecycle): Flow<T> {
return flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
}
// 使用
viewModel.user.observeIn(lifecycle).collect { ... }
// 配合 collectAsStateWithLifecycle()
迁移建议
css
新项目 → 用 Flow
旧项目改造 →
├── ViewModel 层 → 尽量迁移到 Flow
├── XML 布局 observe → 可留 LiveData,封装 Flow→LiveData 扩展
└── Compose 项目 → 必须 Flow
八、一张图总结
scss
冷流(Cold)
┌─────────────┐
│ flow {} │ ← 懒执行,每次 collect 独立
│ flowOf() │
│ asFlow() │
│ callbackFlow│
└──────┬──────┘
│
┌──────▼──────┐ ┌─────────────┐
│ shareIn() │ ───────→ │ SharedFlow │ ← 广播
│ stateIn() │ ───────→ │ StateFlow │ ← 状态
└─────────────┘ └─────────────┘
热流(Hot)
操作符链条:
flow { emit(urls) }
↓ .flatMapLatest { api.fetch(it) }
↓ .debounce(300)
↓ .map { it.toDomain() }
↓ .catch { emit(defaultValue) }
↓ .buffer(64) ← 处理背压
↓ .flowOn(Dispatchers.IO) ← 指定线程
↓ .stateIn(scope) ← 冷→热
↓ collect / launchIn ← 启动消费
希望这篇文章能帮你看清 Kotlin Flow 的全貌。从冷到热,从操作符到背压,再到和 LiveData 的取舍------理解这些,Flow 就不再是黑盒了。