Flow基础:冷流、热流与构建器

在讲泄漏之前,先得把Flow这个基础概念打扎实。我见过不少同学上来就用Flow,结果连冷流热流都分不清,写出来的代码要么莫名其妙不触发,要么一不留神就泄露。说白了,你如果不懂它的“脾气”,就控制不住它。

Flow是什么?它就是一个异步数据流。你可以把它想象成一根水管,数据像水一样从源头流到下游的观察者手里。但这根水管有“冷”和“热”两种模式,搞混了就会出问题。

冷流 vs 热流

我个人习惯这么理解:冷流是“懒”的,热流是“勤快”的。

  • 冷流(Cold Flow):每次有新的收集者(collect)时,都会重新执行上游的数据生产逻辑。也就是说,每个订阅者看到的是一份独立的数据序列。
  • 热流(Hot Flow):数据生产是独立的,不管你收不收集,数据都在产生。后来的收集者只能看到它开始收集之后的最新数据(或者部分历史数据)。

一句话记牢:冷流是“有人看才演”,热流是“不管你爱不爱看,我都在演”。

我在项目中遇到过好几次泄漏,就是因为把冷流当热流用了,或者反过来。比如你用flow{...}构建一个冷流,结果在Activity里启动了多次collect,数据重复触发,界面刷乱了。

热流呢?最常用的是SharedFlow和StateFlow。SharedFlow适合事件分发(比如按钮点击后的弹窗通知),StateFlow适合状态持有(比如UI状态)。StateFlow其实就是SharedFlow的一个特化版本,它总是保存最近一个值,而且初始化时必须给一个默认值。

Flow构建器

Kotlin协程提供了几种构建Flow的方式,我按使用频率排个序:

构建器说明典型场景
flow { ... }最基础的冷流构建器,使用emit发射数据网络请求、数据库查询的序列
flowOf(...)将固定值集合转成Flow测试数据、静态列表
asFlow()将集合、Sequence、其他类型转成Flow从List或Range派发数据
callbackFlow { ... }将回调式异步API转为Flow传感器、位置监听、旧的callback接口
channelFlow { ... }类似callbackFlow,但使用Channel发送,支持背压需要并发或挂起操作的回调转换

嗯,这里要注意:callbackFlow和channelFlow是热流吗?不是,它们本质上还是冷流——每次收集才会启动内部逻辑。但它们内部用了Channel,所以如果你不小心,容易造成资源泄漏。我曾经在callbackFlow里没有在awaitClose中清理监听器,导致Activity销毁后回调依然触发,直接崩了。

常用操作符

操作符就是Flow上的“管道配件”,用来变换、过滤、合并数据流。我用几个经典场景来串讲:

// 1. map — 转换数据
flowOf(1, 2, 3)
    .map { it * 2 }         // 2, 4, 6
    .collect { println(it) }

// 2. filter — 过滤
flowOf(1, 2, 3, 4, 5)
    .filter { it % 2 == 0 } // 2, 4
    .collect { ... }

// 3. debounce — 防抖,用来处理快速变化的值(比如搜索框输入)
flow { 
    for (i in 1..10) {
        emit(i)
        delay(100)
    }
}
.debounce(200)  // 最后只输出10
.collect { ... }

// 4. zip — 合并两个流,一对一配对
val flowA = flowOf("a", "b", "c")
val flowB = flowOf(1, 2, 3)
flowA.zip(flowB) { a, b -> "$a$b" } // a1, b2, c3
    .collect { ... }

// 5. flatMapConcat / flatMapMerge / flatMapLatest — 展平
// 比如一个搜索事件,每次触发一个新的网络请求,用flatMapLatest保证只有最新的请求生效
searchQueryFlow
    .flatMapLatest { query -> api.search(query) }
    .collect { result -> updateUI(result) }

关于异常处理:冷流中的异常可以用catch操作符捕获,但它只能捕获它之前的异常,不能捕获下游。如果你需要“重试”,用retry或retryWhen。我曾经犯过一个错:直接在collect里try-catch,结果流因为异常终止了,后续数据全丢了。正确姿势是在上游用catch处理并emit一个兜底值。

操作符分为中间操作符和末端操作符。中间操作符返回的是新的Flow(懒执行),末端操作符触发执行(如collect、toList、single、count)。你想想看,如果你只调了.map().filter()但没有collect,数据流永远不会启动——这很符合冷流的“懒”本质。

再敲一次黑板:冷流每次collect都会重新执行上游代码。如果一个flow块里包含网络请求,你collect两次就会发两次请求。如果你只想共享结果,请转成热流(shareIn / stateIn),或者直接用StateFlow。

Flow 核心知识体系 Flow 冷流(Cold Flow) 热流(Hot Flow) flow { ... } flowOf(...) callbackFlow SharedFlow StateFlow shareIn / stateIn 操作符 map / filter debounce / throttle flatMapLatest ⚠ 冷流每次 collect 都会 重新执行上游代码

这张图把Flow分为三大块:冷流构建器、热流实现、操作符。你在写代码时,先想清楚数据流的性质——它是冷还是热?然后选择合适的构建器,最后用操作符搭出你需要的逻辑管道。

我记得有一个经验:如果某个数据流在多个订阅者之间需要共享计算结果(比如同一个网络请求的结果),就转成热流;如果是每个订阅者独立需要完整数据(比如数据库的拉取列表),用冷流。搞反了轻则浪费资源,重则泄漏崩溃。