Kotlin Flow 从冷到热:一篇撑起面试的流式编程指南

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 函数,消费者不 collectemit 就挂起------生产者等着消费者,不需要额外机制。

热流的背压更棘手:因为生产者和消费者解耦了,必须靠 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 就不再是黑盒了。

相关推荐
今日无bug2 小时前
深入理解 JavaScript 变量提升(Hoisting)
javascript·面试
HeiSenBerg2 小时前
Android LiveData 原理完整剖析:从订阅、分发到粘性事件
面试
为你学会写情书2 小时前
为什么弹窗状态不该放在全局Store?React性能优化面经复盘与实战指南
面试
众人皆醒我独醉2 小时前
什么是输入上下文、MCP、Agent、Skills?—— 四个让你用不好 AI 的概念,一次讲清楚
面试·ai编程
liang_jy14 小时前
文件管理(一)—— 初识文件管理
面试·操作系统
liang_jy14 小时前
文件管理(二)—— 文件结构
面试·操作系统
HeiSenBerg19 小时前
Android Lifecycle 原理完整剖析:从监听、事件分发到状态驱动
面试
CoderYanger20 小时前
A.每日一题:1979. 找出数组的最大公约数
java·程序人生·算法·leetcode·面试·职场和发展·学习方法
HeiSenBerg20 小时前
LeakCanary 原理完整剖析:从初始化到堆分析
面试