
前言
金丹已凝,你对协程的并发、调度、异常处理已驾轻就熟。你能用 async 并发请求多个接口,能用 supervisorScope 隔离局部失败,能用 withContext 自如切换线程。你写的代码,既安全又优雅。
但现在,你遇到了一个新的挑战:
产品经理要求你实现一个
"实时搜索"功能------用户在输入框中每输入一个字符,就发起一次网络请求,展示搜索建议。你还得处理各种边界:去抖动(避免过快请求)、取消上一个未完成的请求、错误重试、结果去重......
你尝试用 launch + Channel 实现,发现代码迅速膨胀。你想起 RxJava 里有 debounce、switchMap、distinctUntilChanged 等操作符,但引入整个 RxJava 又显得太重。
Kotlin 协程给出的答案,是一个叫做 Flow 的响应式流框架。
Flow是Kotlin协程官方提供的冷流 实现。它允许你以声明式的方式处理连续发射的多个值,并天然集成了协程的挂起、取消、异常处理机制。
如果说 suspend 函数是"单次异步调用"的终极方案,那么 Flow 就是"连续数据流"的终极方案。它填补了协程生态中响应式编程的最后一块拼图。
本讲是元婴境的初阶修炼。你将:
- 理解
Flow是什么,以及它与LiveData、RxJava、Sequence的区别。 - 掌握冷流的本质:为什么
Flow是"懒加载"的? - 学会用
flow {}构建器创建自己的数据流。 - 了解
collect终端操作符和流的基本生命周期。
准备好元婴初啼,感受数据流动了吗?我们开始。
操千曲 而后晓声,观千剑 而后识器。虐它千百遍 方能通晓其真意。
什么是 Flow?
在 Kotlin 协程的官方定义中:
Flow<T>是一个冷流 接口,代表一个可以异步、按顺序发射多个值 的数据流。它使用挂起函数来生产与消费数据 ,天然支持背压 和结构化并发。
Flow 并不是 Kotlin 协程的独创概念。如果你熟悉 RxJava,可以把 Flow 理解为协程版的 Observable,但更轻量、与协程生命周期深度绑定、API 更简洁。
Flow 的核心特征:
- 冷流 :
Flow仅在被收集(collect)时才开始执行。每次收集都会触发一次全新的数据生产。 - 挂起:生产者和消费者都可以是挂起函数,天然支持异步操作而不阻塞线程。
- 可取消 :
Flow的收集过程响应协程的取消信号,无需额外处理。 - 结构化并发 :
Flow的生命周期受收集它的协程作用域约束。
为什么需要 Flow?Flow vs 其他方案
在 Flow 出现之前,Android 开发者处理连续数据流的方案主要有三种。它们各有痛点。

| 方案 | 优点 | 痛点 | 与协程集成 |
|---|---|---|---|
| 回调接口 | 简单直接 | 回调地狱,生命周期需手动管理 | 需手动适配 |
| LiveData | 生命周期感知,自动清理 | 操作符少,不支持挂起,强制主线程处理数据 | 需 liveData 构建器 |
| RxJava | 操作符极丰富,功能强大 | 学习曲线陡,依赖重,响应式类型繁多 | 需 rxjava-coroutines 适配 |
| Flow | 协程原生,操作符丰富,轻量 | 相对较新,生态仍在完善 | 无缝集成 |
Flow 的核心价值 :它让你用一套统一的协程心智模型 处理所有异步场景------单次异步用 suspend fun,连续数据流用 Flow。不再需要在不同框架之间来回切换。
冷流的本质:为什么 Flow 是"懒"的?
Flow 最区别于传统 LiveData 或 Channel 的特性是:它是冷的。
冷流 意味着:Flow 的代码块在被收集(collect)之前不会执行。每一次收集都会触发一次全新的数据生产,互不干扰。
用日常生活的类比:

- 热流 像广播电台。无论有没有人收听,电台都在持续发射信号。后来打开收音机的人,只能听到此刻之后的内容。
- 冷流 像点播视频。只有当用户点击"播放"时,视频才开始加载和播放。每个用户点击播放,都会触发一次独立的视频流。
代码验证:
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
fun simpleFlow(): Flow<Int> = flow {
println("Flow 开始生产")
emit(1)
emit(2)
emit(3)
}
fun main() = runBlocking {
val flow = simpleFlow()
println("Flow 已创建,但尚未生产")
println("第一次收集:")
flow.collect { value -> println("收集到:$value") }
println("第二次收集:")
flow.collect { value -> println("收集到:$value") }
}
输出:
css
Flow 已创建,但尚未生产
第一次收集:
Flow 开始生产
收集到:1
收集到:2
收集到:3
第二次收集:
Flow 开始生产
收集到:1
收集到:2
收集到:3
每次 collect 都会触发 flow {} 块的重新执行。这就是冷流的核心特征。
为什么冷流很重要?
- 资源节省:不收集就不执行,不会白白消耗 CPU/网络。
- 可重放:每次收集都是独立的数据流,适合处理"每个订阅者都需要最新数据"的场景(如网络请求)。
- 线程安全:不同收集者的数据生产彼此隔离,无需担心并发问题。
flow {} 构建器:创造你的第一个 Flow
flow {} 是创建 Flow 最基础的构建器。它的签名如下:
kotlin
public fun <T> flow(
block: suspend FlowCollector<T>.() -> Unit
): Flow<T>
在 block 内部,你可以使用 emit() 函数发射值。emit 是一个挂起函数,这意味着 Flow 的生产过程天然支持异步操作。
kotlin
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
/**
* 创建一个每秒发射一个数字的 Flow
*/
fun countdownFlow(from: Int): Flow<Int> = flow {
for (i in from downTo 1) {
delay(1000) // 模拟耗时操作
emit(i) // 发射当前值
}
}
在 ViewModel 中使用 Flow
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class CountdownViewModel : ViewModel() {
// 暴露 Flow,而非 LiveData
val countdownFlow: Flow<Int> = countdownFlow(10)
fun startCountdown() {
viewModelScope.launch {
countdownFlow.collect { value ->
// 这里的代码会在每个值发射时执行
println("倒计时:$value")
// 更新 UI 状态(后续讲 StateFlow 时会优化)
}
}
}
}
Flow 的生命周期与取消
Flow 的收集过程天然响应协程的取消。当收集 Flow 的协程被取消时,Flow 的生产会停止。
kotlin
fun main() = runBlocking {
val job = launch {
countdownFlow(10).collect { value ->
println(value)
}
}
delay(3500) // 等待 3.5 秒
job.cancelAndJoin() // 取消收集协程
println("收集已取消")
}
// 输出只会到 7 左右,因为协程被取消了
collect 终端操作符:点燃冷流的火种
collect 是 Flow 最基础的终端操作符 。它的作用是从 Flow 中收集每一个发射的值,并对每个值执行指定的操作。
终端操作符 :触发
Flow开始执行的操作符。没有终端操作符,Flow永远不会运行。
collect 是一个挂起函数,它会挂起当前协程 直到 Flow 完成(发射完所有值或抛出异常)。
kotlin
public suspend fun Flow<*>.collect(collector: FlowCollector<*>)
除了 collect,Flow 还提供了一些便捷的终端操作符:
| 操作符 | 作用 | 适用场景 |
|---|---|---|
collect |
收集每一个值 | 需要处理每一个发射值 |
collectLatest |
只处理最新的值,如果新值到来时上一个值还没处理完,则取消上一个处理 | 实时搜索、UI 刷新 |
toList |
将 Flow 的所有值收集到一个 List 中 | 需要一次性获取所有结果 |
toSet |
收集到一个 Set 中 | 需要去重 |
first |
只收集第一个值,然后取消 Flow | 只关心首次结果 |
reduce |
对值进行累加计算 | 求和、拼接等 |
fold |
带初始值的累加 | 复杂聚合 |
kotlin
// 使用 toList 一次性收集
suspend fun getAllNumbers(): List<Int> = countdownFlow(5).toList()
// 返回 [5, 4, 3, 2, 1]
// 使用 first 只取第一个
suspend fun getFirstNumber(): Int = countdownFlow(5).first()
// 返回 5
终端操作符分类图:

实战:用 Flow 实现实时搜索
让我们用一个完整的 Android 实战案例来巩固 Flow 的基础用法。场景是:用户在 TextField 中输入关键词,应用实时展示搜索建议。
关键需求:
- 每次输入都触发搜索请求。
- 避免过快触发(去抖动)。
- 取消上一个未完成的请求。
- 忽略重复的关键词。
这些在 Flow 的世界里只需要几个操作符即可实现(本讲先展示基础架构,操作符细节将在元婴境·中阶深入)。
kotlin
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
class SearchViewModel(
private val searchRepo: SearchRepository
) : ViewModel() {
// 输入框的文本变化,使用 MutableStateFlow 作为输入源
private val queryFlow = MutableStateFlow("")
// 搜索结果状态
var searchResult by mutableStateOf<List<String>>(emptyList())
private set
var isLoading by mutableStateOf(false)
private set
init {
viewModelScope.launch {
queryFlow
.debounce(300) // 去抖动:300ms 内无新输入才向下传递
.filter { it.length >= 2 } // 至少两个字符才搜索
.distinctUntilChanged() // 忽略连续相同的关键词
.flatMapLatest { query -> // 切换为新请求,取消旧的
flow {
isLoading = true
emit(searchRepo.search(query))
isLoading = false
}
}
.catch { e ->
// 处理异常
searchResult = emptyList()
isLoading = false
}
.collect { result ->
searchResult = result
}
}
}
fun onQueryChanged(newQuery: String) {
queryFlow.value = newQuery
}
}
配合 Compose UI:
kotlin
@Composable
fun SearchScreen(viewModel: SearchViewModel = viewModel()) {
Column(modifier = Modifier.padding(16.dp)) {
TextField(
value = "",
onValueChange = { viewModel.onQueryChanged(it) },
placeholder = { Text("输入关键词搜索") },
modifier = Modifier.fillMaxWidth()
)
if (viewModel.isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
}
LazyColumn {
items(viewModel.searchResult) { item ->
Text(item, modifier = Modifier.padding(8.dp))
}
}
}
}
这个示例展示了 Flow 的强大:用声明式的链式调用,清晰地表达了复杂的异步逻辑。这正是 Flow 相比手动管理协程的压倒性优势。
常见错误与避坑指南
错误 1:在 flow {} 块内调用挂起函数前未处理取消
kotlin
// ❌ 错误:如果在 emit 前执行了不支持取消的阻塞操作,协程无法及时响应取消
fun badFlow(): Flow<Int> = flow {
Thread.sleep(5000) // 阻塞,不响应取消!
emit(1)
}
正确做法 :使用挂起函数 delay 或支持取消的 API。
kotlin
fun goodFlow(): Flow<Int> = flow {
delay(5000) // 挂起,响应取消
emit(1)
}
错误 2:在 ViewModel 中直接 collect 而不使用 launch
kotlin
// ❌ 错误:collect 是挂起函数,不能直接在 init 中调用
class MyViewModel : ViewModel() {
init {
someFlow.collect { } // 编译错误!
}
}
正确做法 :在 viewModelScope.launch 中收集。
kotlin
class MyViewModel : ViewModel() {
init {
viewModelScope.launch {
someFlow.collect { }
}
}
}
错误 3:混淆冷流和热流,期望多次收集共享同一数据源
kotlin
val flow = flow { emit(api.getData()) }
// 两次收集会触发两次网络请求!
flow.collect { }
flow.collect { }
如果你需要共享同一数据源,应该使用 StateFlow 或 SharedFlow(热流),这将在化神境讲解。
最佳实践
-
暴露 Flow,不暴露可变状态 :在 ViewModel 中,对外暴露
Flow<T>而非MutableStateFlow,保持封装性。 -
使用
catch操作符统一处理异常 :不要在每次collect处写try-catch,而是用flow.catch { }在管道中处理。 -
在 ViewModel 中使用
stateIn将冷流转换为热流:避免重复的网络请求,确保 UI 重组时不会触发多次收集。 -
理解冷流的"每次收集独立执行" :如果你需要缓存结果,使用
stateIn或shareIn。 -
Flow 的构建器内不要做太重的工作 :
flow {}块应该专注于发射值,复杂逻辑封装在单独的挂起函数中。
总结与下回预告
恭喜,你已成功凝结元婴,掌握了 Flow 的基础用法!
本讲核心收获:
Flow是协程原生的冷流实现,用于处理连续发射的异步数据。- 冷流是"懒"的:只在被收集时才执行,每次收集独立运行。
flow {}构建器 +emit()发射值,collect终端操作符触发执行。- Flow 天然集成协程的取消、异常处理和结构化并发。
- Flow 相比回调、LiveData、RxJava 有显著优势,是协程生态响应式编程的首选。
在下一讲 【元婴境·中阶】 中,我们将深入学习 Flow 的中间操作符:map、filter、transform、take 等。届时你会明白:
- 如何像搭积木一样用操作符构建数据处理管道?
flatMapConcat、flatMapMerge、flatMapLatest的区别与应用场景?- 如何处理 Flow 中的异常和完成事件?
【当前境界修为面板】
| 当前境界 | 修炼技能 | 修炼进度 | 修炼心得 |
|---|---|---|---|
| 元婴境 · 初阶 | 1、Flow 冷流心法 2、flow {} 创造术 3、collect 收集诀 |
当前进度 :25% 修为 :250/1000 下一突破 :[元婴境 · 中阶] (需领悟:Flow 操作符大全) |
冷流如点播视频------不点击播放就不加载,每次播放都是从头开始。 |
【本讲思考题】
1、表象题:以下代码会打印什么?
kotlin
runBlocking {
val flow = flow {
println("Start")
emit(1)
emit(2)
}
println("Before collect")
flow.collect { println(it) }
flow.collect { println(it) }
}
2、场景题 :你需要实现一个"每隔 1 秒轮询一次服务器获取最新状态,直到状态变为 COMPLETED"的功能。如何用 flow {} 和 collect 实现?写出核心代码。
3、原理题 :flow {} 构建器内部是如何保证线程安全的?为什么多个收集者同时收集同一个 Flow 不会互相干扰?请结合 SafeFlow 类简述。
道友,元婴初成,你已能感知数据流动的韵律。下一讲,我们将掌握 Flow 的操作符,让数据在管道中自由变幻。元婴境·中阶见。
欢迎一键四连 (
关注+点赞+收藏+评论)