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 实例,以为数据会「缓存」住。结果每次滑动都触发了一次网络请求。后来才意识到,冷流就是「用完即弃」的,想要缓存得用 shareInstateIn

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 都是独立执行的。这就是冷流的魅力——你完全控制数据的生产和消费时机。

进阶提示:如果你想要「热流」的效果,比如事件总线、状态管理,那就得用 SharedFlowStateFlow。别急,咱们后面有专门的一章讲这个。

好了,这一节的内容就到这。你只要记住:Flow 默认是冷的,三种构建方式各有千秋。下一节咱们会深入 Flow 的操作符,那才是真正发挥响应式编程威力的地方。