6、Flow基础:Flow是什么、冷流与热流的区别、Flow的构建与终端操作符
好,咱们今天来聊聊 Flow。说实话,刚接触协程那会儿,我对 Flow 是有点抵触的——「不就是个异步序列吗?我用 LiveData 加个线程池不也能搞定?」后来在项目里踩了几次坑,才真正体会到 Flow 的设计有多巧妙。
Flow 是什么?说白了,它是一个冷数据流。你可以把它想象成一条水管,数据像水流一样从上游流下来,下游有人打开水龙头(collect),水才会开始流。没人接水,水管就是空的。
冷流与热流的区别
这个区别我当年理解了好久。咱们直接看对比:
| 特性 | 冷流(Flow) | 热流(StateFlow / SharedFlow) |
|---|---|---|
| 数据生产时机 | 有人订阅才开始 | 不管有没有订阅,都在生产 |
| 每个订阅者 | 收到完整数据序列 | 可能只收到订阅之后的数据 |
| 典型场景 | 网络请求、数据库查询 | UI 状态、事件总线 |
| 是否共享 | 不共享,每个订阅者独立 | 共享,多个订阅者共用 |
我举个例子你就明白了。冷流就像 Netflix——你点开一部电影,它才开始下载播放。热流就像电视台——不管你看不看,节目一直在播。你打开电视只能看到当前正在播的内容,看不到之前的。
核心要点:Flow 默认是冷流。每个终端操作符(比如 collect)都会触发一次全新的数据生产。同一个 Flow 被 collect 两次,数据会生产两次。
我个人习惯:网络请求用冷流,UI 状态用热流。别搞混了,否则会出现「请求发了两次」或者「状态丢失」的诡异 bug。
Flow 的三种构建方式
Kotlin 给我们提供了三种最常用的构建方式,我按使用频率排个序:
1. flowOf
最简单的方式,适合已知的固定数据:
val numberFlow: Flow<Int> = flowOf(1, 2, 3, 4, 5)
嗯,就这么简单。你传什么,它就发什么。我在写单元测试时经常用这个,模拟一个有限的数据流。
2. asFlow
把现有集合或序列转成 Flow:
val listFlow: Flow<Int> = listOf(1, 2, 3).asFlow()
val arrayFlow: Flow<Int> = intArrayOf(1, 2, 3).asFlow()
这个我用的最多的地方是——把数据库返回的 List 转成 Flow,然后做链式操作。你想想看,本来要写一堆 for 循环,现在一个 map 就搞定了。
3. flow {} 构建器
最灵活的方式,可以发射任意数据,甚至异步数据:
val userFlow: Flow<User> = flow {
// 发射第一个数据
emit(fetchUserFromCache())
// 模拟网络请求
val user = fetchUserFromNetwork()
emit(user)
// 可以发射多个
for (i in 1..3) {
emit(updateUserStatus(i))
delay(1000) // 注意:这里可以挂起
}
}
我曾经踩过的坑:在 flow {} 里直接调用了阻塞式 IO 操作,比如 File.readText()。结果 collect 的时候卡住了主线程。记住:flow {} 的代码块是挂起上下文,但如果你在里面调了阻塞方法,它不会自动帮你切线程。需要配合 flowOn(Dispatchers.IO) 使用。
Flow 的终端操作符
终端操作符,就是触发数据流动的「开关」。没有它们,Flow 就是个摆设。
collect
最常用的终端操作符,没有之一:
lifecycleScope.launch {
flowOf(1, 2, 3).collect { value ->
Log.d("Flow", "收到: $value")
}
}
collect 是一个挂起函数,它会一直阻塞直到 Flow 结束(或者协程被取消)。
toList
把 Flow 的所有数据收集到一个 List 中:
val result: List<Int> = flowOf(1, 2, 3).toList()
// result = [1, 2, 3]
这个在测试里特别好用。我经常用它来验证 Flow 发射的数据是否符合预期:
@Test
fun `test user flow`() = runTest {
val flow = userRepository.getUsers()
val users = flow.toList()
assertEquals(3, users.size)
}
first
只取第一个数据,然后取消 Flow:
val firstNumber: Int = flowOf(1, 2, 3).first()
// firstNumber = 1
也可以带条件:
val firstEven: Int = flowOf(1, 2, 3, 4).first { it % 2 == 0 }
// firstEven = 2
这个操作符我经常用在「只需要最新状态」的场景。比如用户登录后,我只想拿一次用户信息,不需要持续监听。
知识体系总览
下面这张图帮你理清 Flow 的核心脉络:
这张图把 Flow 的三个核心维度串起来了。你从上往下看:先搞清楚冷热流,再掌握构建方式,最后用终端操作符触发执行。这三步走完,Flow 的基础就算打牢了。
我个人建议:刚开始学 Flow,先死磕 flow {} 和 collect 这两个。其他操作符都是锦上添花。把这两个用熟了,后面学 map、filter、catch 这些中间操作符会轻松很多。
好了,Flow 的基础就这些。下一节咱们会深入中间操作符,到时候你会发现——原来 Flow 的链式调用这么爽。
公众号:蓝海资料掘金营,微信deep3321