2、Flow 基础概念:冷流 vs 热流,Flow 的三种构建方式
好,咱们正式开始聊 Flow 了。说实话,我第一次接触 Flow 的时候,脑子里全是 RxJava 的影子。但用了一段时间后,我发现 Flow 的设计更「Kotlin」,也更贴近协程的思维模式。
这一节,咱们先把最核心的两个概念搞清楚:冷流和热流。然后我会带你手把手写出你的第一个 Flow。
2.1 冷流 vs 热流:这俩到底有啥区别?
很多新手一上来就被「冷」「热」搞懵了。其实没那么玄乎。我打个比方你就懂了。
冷流就像 Netflix 上的电影。你点播放,它才开始下载、播放。你不点,它啥也不干。每个观众都有自己的进度条,互不干扰。
热流就像电视台直播。不管你有没有打开电视,节目一直在播。你打开电视,看到的是当前画面,之前的剧情就错过了。所有观众看到的是同一帧画面。
嗯,这个比喻应该够形象了。咱们看看代码里怎么体现。
核心区别一句话总结:
- 冷流:没有消费者就不生产数据。每个消费者收到完整的数据序列。
- 热流:不管有没有消费者,数据都在生产。新消费者只能收到订阅之后的数据。
在 Kotlin 里,Flow 默认是冷流。这一点和 RxJava 的 Observable 不一样——RxJava 的 Observable 默认是热的。我刚开始迁移的时候,就在这上面栽过跟头。
咱们写个例子感受一下:
// 冷流:每次 collect 都会重新执行代码块
fun coldFlow(): Flow<Int> = flow {
println("Flow 开始执行")
for (i in 1..3) {
emit(i)
}
}
fun main() = runBlocking {
val flow = coldFlow()
println("第一次收集:")
flow.collect { println(it) }
println("第二次收集:")
flow.collect { println(it) }
}
猜猜输出是什么?
第一次收集:
Flow 开始执行
1
2
3
第二次收集:
Flow 开始执行
1
2
3
看到了吗?Flow 开始执行 打印了两次。每次 collect,Flow 的代码块都会重新跑一遍。这就是冷流的典型特征。
我曾经踩过的坑:在 RecyclerView 的列表刷新时,我错误地复用了同一个 Flow 实例,以为数据会「缓存」住。结果每次滑动都触发了一次网络请求。后来才意识到,冷流就是「用完即弃」的,想要缓存得用 shareIn 或 stateIn。
2.2 Flow 的三种构建方式
了解了冷热流,咱们来看看怎么创建 Flow。Kotlin 提供了三种最常用的方式,我按使用频率给你排个序。
2.2.1 flow{}:最灵活的构建器
这是我最常用的方式。它接受一个挂起代码块,你可以用 emit() 发射任意多个值。
val userFlow: Flow<User> = flow {
val users = fetchUsersFromApi() // 挂起函数
for (user in users) {
emit(user) // 逐个发射
}
}
这里有个细节要注意:flow{} 的代码块是挂起上下文,所以你可以直接调用其他挂起函数。但你不能用 withContext 切换调度器——嗯,这个咱们后面讲上下文时再细说。
我的习惯:如果发射逻辑比较复杂,比如需要从多个数据源合并数据,我会用 flow{}。它最灵活,也最容易调试。
2.2.2 flowOf:固定数据的最简方式
如果你有一组已知的数据,flowOf 是最简洁的选择。它直接接收可变参数,返回一个 Flow。
val numberFlow: Flow<Int> = flowOf(1, 2, 3, 4, 5)
val stringFlow: Flow<String> = flowOf("Kotlin", "Flow", "响应式")
说白了,这就是个语法糖。底层其实还是调用了 flow{},但写起来清爽多了。
我个人习惯在单元测试里大量使用 flowOf。比如测试一个 map 操作符:
fun testMapOperator() = runBlocking {
val result = flowOf(1, 2, 3)
.map { it * 2 }
.toList()
assert(result == listOf(2, 4, 6))
}
你看,三行代码就搞定了测试数据。不用 mock,不用复杂的 setup。
2.2.3 asFlow:集合/序列的快速转换
这个扩展函数可以把各种集合类型转成 Flow。你想想看,平时我们处理列表数据时,是不是经常需要遍历?asFlow 就是干这个的。
val list = listOf("A", "B", "C")
val listFlow: Flow<String> = list.asFlow()
val set = setOf(1, 2, 3)
val setFlow: Flow<Int> = set.asFlow()
val sequence = sequenceOf(1.0, 2.0, 3.0)
val seqFlow: Flow<Double> = sequence.asFlow()
它甚至支持 IntRange:
val rangeFlow: Flow<Int> = (1..10).asFlow()
我记得有一次做分页加载,后端返回了一个列表,我需要逐条处理。直接用 list.asFlow() 配合 map 操作符,代码干净得不得了。
2.3 三种方式怎么选?
我整理了一个表格,方便你快速决策:
| 构建方式 | 适用场景 | 特点 |
|---|---|---|
flow{} |
需要自定义发射逻辑、异步操作、条件判断 | 最灵活,可调用挂起函数 |
flowOf |
已知固定数据、单元测试、简单演示 | 最简洁,代码量最少 |
asFlow |
集合/序列转 Flow、批量数据处理 | 转换最方便,零开销 |
一句话总结:
数据是动态生成的用 flow{},数据是静态写死的用 flowOf,数据已经在集合里了用 asFlow。
2.4 一个小练习
咱们来写个综合例子,把三种方式都用上:
fun main() = runBlocking {
// 1. flowOf:直接发射三个字符串
flowOf("Hello", "Kotlin", "Flow")
.collect { println(it) }
// 2. asFlow:把列表转成 Flow
listOf(10, 20, 30).asFlow()
.collect { println(it) }
// 3. flow{}:模拟网络请求
flow {
emit("请求中...")
delay(1000) // 模拟网络延迟
emit("请求完成")
}.collect { println(it) }
}
跑一下看看,你会发现每个 Flow 都是独立执行的。这就是冷流的魅力——你完全控制数据的生产和消费时机。
进阶提示:如果你想要「热流」的效果,比如事件总线、状态管理,那就得用 SharedFlow 和 StateFlow。别急,咱们后面有专门的一章讲这个。
好了,这一节的内容就到这。你只要记住:Flow 默认是冷的,三种构建方式各有千秋。下一节咱们会深入 Flow 的操作符,那才是真正发挥响应式编程威力的地方。