Kotlin协程Flow异步测试实战教程 - 第3节
Flow 是什么?挂起函数还不够吗?
讲到 Kotlin 协程,很多人第一反应是 suspend 函数。没错,挂起函数确实能解决「异步等待」的问题。但我在项目中遇到过这样的场景:一个网络请求返回的不是单个结果,而是连续的数据流,比如 WebSocket 推送或者数据库实时更新。这时候挂起函数就有点力不从心了——它只能返回一次,不能持续发射。
Flow 就是为解决这个问题而生的。打个比方:挂起函数像是一次性饮料机,按一下出一杯;而 Flow 像水龙头,打开后持续出水,直到你关掉它。
挂起函数 vs Flow:关键差异
先看个实际对比,你就能体会到两者的区别了。
挂起函数(一次性返回)
suspend fun fetchUser(): User {
delay(1000) // 模拟网络请求
return User("Kotlin")
}
// 调用一次,返回一个值
val user = fetchUser()
Flow(连续发射多个值)
fun fetchUsers(): Flow<User> = flow {
for (i in 1..5) {
delay(1000)
emit(User("User $i"))
}
}
// 收集每个发射的值
fetchUsers().collect { user ->
println(user.name)
}
我个人习惯这样理解:挂起函数是「点对点」的,Flow 是「流式」的。Flow 可以发射零个、一个或多个值,而且支持各种操作符,比如 map、filter、catch 等,像 RxJava 那样链式处理数据。
| 特性 | 挂起函数 | Flow |
|---|---|---|
| 返回值个数 | 1 个(或 0 个,抛异常) | 0 ~ N 个 |
| 是否可取消 | 可以,通过协程取消 | 可以,通过协程取消 |
| 操作符支持 | 无 | map, filter, catch, flatMapConcat 等 |
| 协程上下文 | 继承调用者的上下文 | 默认 preserve 上下文,可用 flowOn 切换 |
| 典型场景 | 单个请求、单个数据库查询 | 数据流、消息推送、传感器数据 |
merge 操作符把多次请求拼成一个流,再配合 collect 更新 UI,代码比回调嵌套整洁多了。
冷流 vs 热流:本质区别
Flow 有两个最基础的分类:冷流(Cold Flow)和热流(Hot Flow)。说实话,刚接触时我花了点时间才搞懂,因为它们的行为有点像又不太像。我们先看定义。
冷流(Cold Flow)
冷流的特点是:没有收集者时,不执行任何代码。每次收集,数据都会从头重新发射一次。想象一下 DVD 光盘——你放进去播放,它就从头开始放;你拿走,它就不工作了。
常见的冷流有:
flow { ... }channelFlow { ... }- 通过
.asFlow()转换来的流
val coldFlow = flow {
println("Flow 开始执行")
emit(1)
emit(2)
}
// 第一次收集
coldFlow.collect { println("收集到 $it") }
// 输出:
// Flow 开始执行
// 收集到 1
// 收集到 2
// 第二次收集,从头再来
coldFlow.collect { println("再次收集 $it") }
// 输出:
// Flow 开始执行
// 收集到 1
// 收集到 2
热流(Hot Flow)
热流就不一样了。它不管有没有人收集,都会持续发射数据。你可以把它想象成直播电视信号——你打开电视就能看到当前画面,但之前的画面就错过了。热流有多个收集者时,大家共享同一份数据流,不会各自重播。
Kotlin 提供了两种标准热流:
- StateFlow:始终持有最新值,类似 LiveData,适合 UI 状态管理。
- SharedFlow:更通用的热流,可以配置重放数量、是否缓存等。
val sharedFlow = MutableSharedFlow<Int>(replay = 1)
// 发射数据(即使还没收集者)
sharedFlow.emit(1)
sharedFlow.emit(2)
// 第一个收集者:会收到 replay 的最后一个值
sharedFlow.collect { println("A 收集到 $it") }
// 输出:A 收集到 2(replay=1 缓存了 2)
// 第二个收集者:同样收到 replay 的值
sharedFlow.collect { println("B 收集到 $it") }
// 输出:B 收集到 2
flow { } 来监听数据库变化,结果每次有观察者时都会重新查询数据库。换成 SharedFlow 后,在数据变化时主动 emit,所有观察者都能实时收到更新,而且不会重复查询。切记:冷流适合「按需计算」的场景,热流适合「广播事件」的场景。
总结:一张图看懂 Flow 家族
下面这张图梳理了 Flow 的核心分类和关系,方便你以后快速回顾。
一句话记住
冷流是「懒加载」的,只有你 collect 了才干活;热流是「勤快的」,一直在那边发射,来一个收集者就给它一份当前数据。我个人偏爱用 StateFlow 做 ViewModel 的状态持有,用 flow { } 做网络请求或数据库查询——前者要持续更新 UI,后者只需要响应一次结果集变化就够了。