🎯 Flow 的设计哲学
Flow 的设计初衷是解决 "异步数据流的统一处理" 问题。在传统的编程模型中,我们处理单个异步结果用 suspend 函数,处理多个同步值用 Sequence/Collection,但处理多个异步到达的值(如数据库观察者、传感器数据、UI 事件等)一直没有统一的工具。RxJava 试图解决这个问题,但学习曲线陡峭,且与 Kotlin 协程的集成不够自然。
Flow 的设计目标:
- 与协程深度集成:Flow 的所有操作都是挂起函数,可以无缝使用协程的取消、结构化并发等特性。
- 冷流:Flow 的生产者代码只在有消费者收集时才执行,避免了不必要的资源消耗。
- 背压透明:Flow 默认是同步的,消费者处理速度直接影响生产者,但提供了缓冲区、合并等操作符来应对速度不匹配。
- 操作符丰富:提供了与 RxJava 类似的操作符(
map、filter、flatMapConcat等),但更简洁、更协程化。 - 可取消:Flow 遵循协程的取消机制,当收集的协程被取消时,Flow 的生产者和中间操作都会自动停止。
🔄 Flow 的核心类型与分类
Flow 主要分为两大类:冷流和热流。
1. 冷流 (Cold Stream)
- 定义:每次收集都会重新执行生产者的代码,数据生产与收集一一对应。
- 代表:
Flow接口本身(通过flow { ... }构建)。 - 特点:
- 无状态,每次收集独立。
- 不会主动发射数据,直到被
collect。 - 适合封装一次性的数据源(如网络请求、数据库查询)。
2. 热流 (Hot Stream)
- 定义:无论是否有收集者,数据都会持续产生,多个收集者共享同一份数据流。
- 代表:
StateFlow、SharedFlow。 - 特点:
- 有状态,可以缓存最新值或事件。
- 多个收集者可以同时订阅,不会触发新的生产者执行。
- 适合 UI 状态持有(
StateFlow)或事件广播(SharedFlow)。
🧱 Flow 的底层原理:协程与 Channel
要深入理解 Flow,必须了解其底层依赖的两大组件:协程挂起和 Channel。
协程挂起
Flow 的所有终端操作符(如 collect)都是挂起函数。当调用 collect 时,当前协程会被挂起,直到流中的所有数据被处理完或流被取消。中间操作符(如 map)也是挂起函数,但它们是内联或生成新的 Flow,不会直接挂起。
Channel
flow { ... } 构建器内部使用了 ProducerScope,它是一个带有 send 方法的协程作用域。当你调用 emit 时,本质上是通过 ProducerScope 内部的 Channel 将数据发送给下游。这个 Channel 是无缓冲的(默认容量为 0),这意味着 emit 会挂起,直到下游准备好接收。这实现了天然的背压——生产者不会比消费者快太多,除非显式引入缓冲。
public fun <T> flow(@BuilderInference block: suspend FlowCollector<T>.() -> Unit): Flow<T> {
return SafeFlow(block) // SafeFlow 继承 AbstractFlow,最终调用 collect 时启动协程执行 block
}
FlowCollector 的 emit 方法是一个挂起函数,在 SafeFlow 的实现中,它会调用 collector 的 emit,而 collector 最终会调用消费者的 onEach 等操作。整个链条都是挂起的,数据通过挂起传递,不涉及线程阻塞。
⚙️ 操作符的实现原理
Flow 的操作符绝大多数都是通过构建新的 Flow 来实现的。例如 map 操作符:
public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = flow {
collect { value ->
emit(transform(value))
}
}
它内部创建了一个新的 Flow,在新的 Flow 的 collect 中,它会调用原始 Flow 的 collect,对每个值应用 transform 后再 emit 出去。这种组合方式使得操作符是惰性的,只有最终 collect 时才会依次执行。
背压处理操作符的原理
buffer:在生产者与消费者之间插入一个带有缓冲区的 Channel。flow.buffer(10)会创建一个新的 Flow,它启动一个单独的协程来收集上游数据并放入缓冲区,而下游的收集则从这个缓冲区读取。这通过channelFlow实现,本质上是开启了并发。conflate:相当于buffer(Channel.CONFLATED),缓冲区只保留最新值。collectLatest:当新值到来时,如果正在处理旧值,会取消旧值的处理协程。它通过coroutineScope和Job的取消来实现。
🔥 热流:StateFlow 与 SharedFlow 的深入理解
StateFlow
StateFlow 是一个状态容器式的热流,它始终持有当前值,并且只向订阅者发送更新后的值。它的核心特性:
- 去重:默认情况下,只有值发生变化(通过
equals比较)时才会发送给订阅者。这通过内部的updateState机制实现,每次更新时检查新旧值是否相等。 - 与 LiveData 对比:
StateFlow是纯 Kotlin 的,不依赖 Android 主线程,完全基于协程,更适合跨平台项目。但它不处理生命周期(需要结合repeatOnLifecycle)。 - 实现原理:
MutableStateFlow内部维护一个value变量和一个版本号。当value更新时,它会通知所有订阅者(通过collect的挂起函数恢复)。订阅者通过collect挂起,并在值变化时被唤醒。
SharedFlow
SharedFlow 是一个事件广播式的热流,它不持有状态,只负责将事件发送给当前订阅的收集者。它的核心特性:
- 重放缓存:可以配置
replay参数,让新订阅者收到之前发送过的若干条事件。 - 溢出策略:可以配置缓冲区大小和溢出策略(
SUSPEND、DROP_OLDEST、DROP_LATEST)。 - 实现原理:
SharedFlow内部维护一个循环缓冲区(ArrayDeque)存储事件,以及一个订阅者列表。当事件emit时,会遍历所有订阅者并恢复其挂起的collect协程。shareIn操作符可以将冷流转换为热流,它会在指定的协程作用域内启动一个收集协程,并将收集到的事件广播给所有订阅者。
🧪 Flow 的异常处理与取消
异常处理
Flow 的异常处理主要通过 catch 操作符。catch 只捕获上游的异常,不捕获下游(collect 内)的异常。它的原理是:当上游抛出异常时,异常会沿着 collect 链传播,直到遇到 catch 操作符。catch 内部可以重新 emit 数据或重新抛出异常。例如:
flow {
emit(1)
throw RuntimeException("error")
}.catch { e ->
emit(-1) // 发送一个默认值
}.collect { println(it) } // 输出 1, -1
如果 collect 内部抛出异常,只能在 collect 外部用 try-catch 捕获。
取消
Flow 的取消遵循协程的结构化并发规则。当 collect 的协程被取消时,Flow 的所有挂起点(如 emit、delay、flow 内部的挂起函数)都会抛出 CancellationException,从而终止整个流。例如:
val job = launch {
flow {
repeat(100) {
delay(100)
emit(it)
}
}.collect { println(it) }
}
delay(500)
job.cancel() // 流会在下一次 emit 或 delay 时取消
📊 Flow 与 RxJava 的对比
| 特性 | Flow | RxJava |
|---|---|---|
| 背压策略 | 默认同步,可用 buffer 等操作符开启缓冲 | 需要显式选择背压策略(如 BackpressureStrategy) |
| 线程调度 | flowOn 切换上游线程,下游线程由 collect 所在协程决定 | subscribeOn 和 observeOn 分别指定订阅和观察线程 |
| 取消机制 | 基于协程取消,自动传播 | 需要手动管理 Disposable |
| 操作符数量 | 丰富但少于 RxJava | 非常丰富,几乎涵盖所有需求 |
| 学习曲线 | 平缓,熟悉协程即可上手 | 陡峭,需要理解多个概念 |
| 与 Android 集成 | 需要 repeatOnLifecycle 处理生命周期 | 需要 RxLifecycle 等库 |
Flow 更适合与协程配合的新项目,而 RxJava 在遗留项目中仍占有一席之地。
🏗️ Flow 在架构中的典型使用模式
1. 数据层:返回 Flow
数据层(Repository)通常提供 Flow<T> 作为数据源,例如 Room 数据库的查询会自动返回 Flow,网络请求也可以包装成 Flow:
fun getUserStream(userId: String): Flow<User> = flow {
while(true) {
val user = api.fetchUser(userId) // 轮询
emit(user)
delay(5000)
}
}.flowOn(Dispatchers.IO)
2. 领域层:使用 Flow 进行业务转换
UseCase 或 Interactor 可以对数据层的 Flow 应用操作符,进行数据转换、合并、过滤等,然后再提供给 UI 层。
3. UI 层:收集 Flow
在 Android 中,UI 层收集 Flow 时需要考虑生命周期安全。推荐使用 repeatOnLifecycle:
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
// 更新 UI
}
}
}
}
}
在 Compose 中,可以直接使用 collectAsStateWithLifecycle()。
🧠 深入理解:Flow 的冷热转换与作用域
冷流转热流
有时我们需要将冷流转换为热流,以便多个订阅者共享同一份数据,或者让数据在后台持续产生。Hilt 提供了 shareIn 和 stateIn 操作符。
shareIn:将冷流转换为SharedFlow。需要指定一个协程作用域,在该作用域内启动一个收集上游的协程,并将收集到的事件广播给所有订阅者。stateIn:将冷流转换为StateFlow。同样需要作用域,并且可以设置初始值。
val sharedFlow = someFlow.shareIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
replay = 1
)
SharingStarted 策略控制着共享的开始和停止时间,WhileSubscribed 表示当有订阅者时开始,当最后一个订阅者消失后延迟一段时间停止。
Flow 与协程作用域
Flow 本身不持有作用域,它的执行依赖于调用 collect 的协程作用域。因此,在 viewModelScope 中启动的 collect 会跟随 ViewModel 的生命周期,而在 lifecycleScope 中启动的会跟随 Activity/Fragment 的生命周期。这种设计使得 Flow 非常安全,不会发生泄漏。
🛠️ 常见陷阱与最佳实践
- 不要在
flow { }中使用launch或async:因为这样会开启新的协程,但这些协程的生命周期不受 Flow 控制,可能导致泄漏或数据竞争。应该使用callbackFlow来处理基于回调的异步 API。 - 正确处理异常:
catch只能捕获上游异常,collect内的异常需要在collect外捕获,或者使用onEach进行预处理。 - 避免在操作符中执行耗时操作:耗时操作应该放在
flowOn指定的调度器中,或者通过map调用挂起函数(但注意挂起函数会阻塞流,除非使用buffer开启并发)。 - 合理选择热流策略:UI 状态用
StateFlow,一次性事件用SharedFlow,不要混用。 - 注意流的取消:如果 Flow 内部有无限循环(如轮询),确保在不再需要时能通过取消协程来停止。
📖 总结
Flow 是 Kotlin 协程体系中处理异步数据流的终极武器。它的设计兼顾了简洁性、强大性和安全性,通过挂起函数实现了天然的背压,通过操作符提供了丰富的组合能力,通过冷热流的区分满足了不同场景的需求。深入理解 Flow 的原理,不仅能让我们写出更优雅的代码,还能避免常见的陷阱。