Flow基本用法:flow builder,emit,collect
Flow 说白了就是 Kotlin 协程里的一种冷数据流。冷是什么意思?就是它不会事先产生数据,只有当你主动去收集(collect)的时候,它才开始干活。我个人习惯把它想象成一个“按需生产”的管道,没人拧开水龙头,它就不流水。
💡 核心三板斧:flow { ... } 创建管道,emit(value) 往管道里丢数据,collect { ... } 在终端打开水龙头接数据。就这么简单。
1. flow builder — 创建管道的起点
用 flow 构建器启动一个协程作用域,在里面写发射逻辑。我刚开始接触时总以为它和 suspend 函数一样,直接在函数里 return 数据就行。后来发现完全不是一回事——Flow 是流式返回,不是一次性返回。
val myFlow: Flow<Int> = flow {
// 这里运行在协程上下文中
for (i in 1..3) {
delay(100) // 模拟耗时
emit(i) // 每次发射一个数
}
}
要注意,flow builder 内部的代码默认是顺序执行的,delay 不会阻塞线程,只挂起协程。这点和 suspend 函数一样。
⚠️ 我曾经踩过一个坑:在 flow builder 里直接调用阻塞式 IO(比如 Thread.sleep),会导致整个协程被卡住。记得换成 delay 或 withContext(Dispatchers.IO) 包装一下。
2. emit — 往管道里扔东西
emit 是 FlowCollector 的扩展函数,本质就是把值交给下游。你可以在 flow builder 里任意调用,甚至多次循环发射。每个 emit 都会挂起当前协程,直到下游 collector 处理完这个值。
关键点:emit 是挂起函数,不是同步调用。这意味着 Flow 天然支持反压——如果下游处理速度慢,上游 emit 会被自动挂起,不会积压数据。
flow {
emit(1)
delay(500)
emit(2)
emit(3) // 连续发射,下游按顺序接到
}
我记得有一次做实时定位,每隔 1 秒 emit 一次经纬度,下游在 collect 里做 UI 更新。因为 emit 是挂起的,就算上游想快点发射,下游没处理完它也快不了。这就避免了数据堆积。
3. collect — 终端收集者
collect 是终端操作符,它会挂起当前协程,直到 Flow 结束(或者协程取消)。每次下游 collect 拿到一个值,可以执行任意逻辑。注意 collect 也是一个挂起函数,只能在协程里调用。
lifecycleScope.launch {
myFlow.collect { value ->
Log.d("Flow", "收到: $value")
// 在这里更新 UI 或写数据库
}
}
你想想看,collect 本身就是一个冷启动的开关——只要你不调用 collect,Flow 里的一行代码都不会执行。所以它很适合做懒加载数据流。
🔥 实战经验:我在写网络请求库时,用 Flow 包装 OkHttp 的 ResponseBody,每次读取到一部分字节就 emit 出去,collect 里负责写入文件或显示进度条。流程很清晰,而且天然支持取消(协程取消时 collect 自动终止)。
4. 完整示例:从数据源到 UI
下面是个模拟股票价格刷新的例子,每隔 500ms 生成一个随机价格。看看 flow builder、emit、collect 怎么配合。
fun stockPriceFlow(): Flow<Double> = flow {
while (true) {
val price = 100.0 + Random.nextDouble() * 10
emit(price)
delay(500)
}
}
// 在 ViewModel 或 Activity 里收集
viewModelScope.launch {
stockPriceFlow().collect { price ->
// 更新 UI(注意要切换到主线程)
withContext(Dispatchers.Main) {
textView.text = "当前价格: $%.2f".format(price)
}
}
}
这个例子很简单,但藏着几个要点:
- while(true) 循环不会死循环——因为 emit 会挂起,collect 取消后整个协程就释放了。
- 千万不要在 collect 里做耗时操作!collect 是挂起函数但不自动切换调度器。如果里面要写数据库,记得用 withContext(Dispatchers.IO)。我吃过苦头,在 collect 里直接做文件读写把主线程拖垮了。
💡 小技巧:如果要取消收集,直接取消所在的协程作用域(比如 viewModelScope.cancel())就行。Flow 会自动感知取消并停止发射,不需要额外写 try-catch。
5. 知识结构速查表
| 组件 | 作用 | 挂起函数? | 调用位置 |
|---|---|---|---|
| flow { ... } | 创建冷数据流 | 是(内部代码块运行在协程) | 普通函数或协程内 |
| emit(value) | 向流中发射一个值 | 是(会挂起直到下游消费) | 只能在 flow builder 内部 |
| collect { ... } | 终端消费流中的值 | 是(会挂起直到流结束) | 只能在协程内调用 |
6. 不要踩的坑
- 不要在 collect 外使用 emit:emit 只能在 flow builder 或 FlowCollector 扩展里调用,普通函数里用会报错。
- collect 也要记得处理异常:如果 flow 内部抛异常,collect 会直接收到,建议用 catch 操作符包裹。
- 连续多次 collect:同一个 Flow 对象可以被多次 collect 吗?当然可以,但每次都是独立的新流,从头开始执行 flow builder。不会共享状态。
嗯,说了这么多,其实 Flow 的基本用法就围绕这三个关键词转。我个人把 flow builder 比作工厂,emit 是传送带上的产品,collect 是工厂门口的收货人。一旦收货人走了(协程取消),传送带也停了。
📌 最简洁的总结:用 flow { } 定义生产逻辑,用 emit 逐个吐出数据,用 collect 在终端一个个消费。三句话搞定 Flow 入门。
这张图把 Flow 的三大步骤串起来了:由 flow builder 启动,经过 emit 发射,最终由 collect 消费。右边还展示了取消机制——协程取消时 collect 停止,上游 emit 自动释放资源。底下的三个特性(冷流、反压、取消安全)是 Kotlin Flow 区别于其他响应式库的核心优势。