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 切换
典型场景 单个请求、单个数据库查询 数据流、消息推送、传感器数据
我的经验:用 Flow 处理列表分页特别方便。我之前写过一个列表页,每次滑到底部触发加载,用 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 的核心分类和关系,方便你以后快速回顾。

Flow 家族结构图 Flow 冷流(Cold Flow) 热流(Hot Flow) flow { } channelFlow { } .asFlow() StateFlow SharedFlow 关键特征 ● 冷流:无收集不执行,每次收集各自独立(像 DVD) ● 热流:持续发射,共享数据,支持重放和缓存(像电视直播) ● 选择依据:数据是「按需计算」还是「持续推送」?前者冷流,后者热流。

一句话记住

冷流是「懒加载」的,只有你 collect 了才干活;热流是「勤快的」,一直在那边发射,来一个收集者就给它一份当前数据。我个人偏爱用 StateFlow 做 ViewModel 的状态持有,用 flow { } 做网络请求或数据库查询——前者要持续更新 UI,后者只需要响应一次结果集变化就够了。

一个小提示:刚学 Flow 的时候,你可能会纠结「什么时候用冷流、什么时候用热流」。别急,记住这个经验法则就好——UI 状态(比如 isLoading、error 消息)适合热流,因为界面随时可能重建;而一次性的数据加载任务(如获取文章列表)适合冷流,可以按需触发。等你多写几个项目,自然就形成直觉了。