Flow数据流与OkHttp泄漏混合教程 - 第1节
Flow 是什么?先从一个痛点说起
我记得刚接触协程那会儿,总被一个问题困扰:
"协程能把异步代码写得像同步一样,那我要做多个异步值的流式处理该怎么办?"
比如监听一个数据库的实时变更,或者接收 WebSocket 推送的消息。用回调?太丑。用 RxJava?项目里已经用了协程,再加个大库总觉得重。这时候 Jetpack 团队推出了 Flow。
Flow 的本质:一套冷数据流 API
说白了,Flow 就是 Kotlin 协程体系下专门用来处理异步数据流的工具。它的核心特点就三个词:
- 冷流 —— 有订阅者才执行生产逻辑
- 挂起式 —— 天然支持协程取消和结构化并发
- 背压友好 —— 默认使用挂起策略处理生产速度 > 消费速度
📌 一句话总结:Flow = 协程版的 RxJava(但更轻量、更 Kotlin 原生)。
我曾经踩过的坑:冷流 ≠ 热流
刚开始用 Flow 时,我总搞混 StateFlow 和普通 Flow。普通 Flow 是冷的,每次 collect 都会重新执行上游代码。而 StateFlow 是热的,数据独立于订阅者存在。
举个我犯错过的例子:
// 错误的写法 —— 每次 collect 都重新请求网络
fun loadData(): Flow<Result> = flow {
emit(api.fetchData()) // 每次 collect 都会执行
}
// 正确做法 —— 用 stateIn 转热流
val data: StateFlow<Result> = loadData().stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(),
initialValue = Result.Loading
)
那次线上出现了重复请求,排查了半天才发现是冷流在作怪。
响应式编程,到底在响应什么?
你想想看,传统的编程模式是你主动去"拉"数据:
- 调用
db.query() - 等待
api.get() - 循环
list.forEach
而响应式编程是反过来——数据主动"推"给你。你只需要声明:当数据变化时,我要做什么。剩下的管道由框架搭好。
💡 有个形象的比喻:传统模式像你去水龙头接水,响应式模式像把水龙头接到你杯子上的管子,水自己流过来。
四个核心支柱
| 概念 | 说明 | Flow 中的体现 |
|---|---|---|
| 数据流 | 按时间顺序产生的异步事件序列 | Flow<T> |
| 操作符 | 对数据流进行变换、过滤、合并 | map, filter, flatMapLatest… |
| 订阅者 | 最终消费数据的地方 | .collect { ... } |
| 背压管理 | 调节生产者与消费者速度不匹配 | 挂起式背压(默认) |
Flow 与 RxJava 的简单对比
我个人的习惯是:新项目一律用 Flow,老项目里的 RxJava 代码除非牵一发而动全身,否则不动。这里给你列几个关键区别:
- 学习曲线:Flow 的 API 更少,且和协程绑定,如果你已经会用协程,几乎零成本上手。
- 取消机制:RxJava 靠
Disposable手动管理,Flow 自动跟随协程作用域取消。 - 线程调度:RxJava 用
subscribeOn / observeOn,Flow 用flowOn配合协程上下文。 - 背压策略:RxJava 有 BUFFER、DROP、LATEST 等策略,Flow 默认挂起,等价于 BUFFER,但更安全(不会丢事件)。
⚠️ 有一点要特别注意:Flow 的 flowOn 只改变上游协程上下文,不会影响下游。而 RxJava 的 observeOn 会切换下游线程。我一开始没意识到这点,在 UI 层 collect 时忘记切回主线程,直接导致崩溃。
一张图看懂 Flow 数据流与响应式核心
下面这张图总结了 Flow 从创建到消费的完整链路,以及相关的响应式概念:
为什么学 Flow 之前先搞懂响应式?
因为 Flow 不是凭空发明的,它是响应式思想在 Kotlin 世界里的「本地化实现」。如果你不理解数据流、操作符、订阅者这三者的关系,写出来的 Flow 代码很容易变成「协程版的回调地狱」。
举个例子,我见过同事这么写:
// ❌ 反模式:用 Flow 包装了一层但还是在回调里嵌套
viewModel.data.onEach { result ->
lifecycleScope.launch {
when (result) {
is Success -> updateUI(result.data)
is Error -> showError(result.msg)
}
}
}.launchIn(lifecycleScope)
这里用 onEach 搭配 launchIn 实现了订阅,但真正处理还是在协程里直接操作 UI,完全没有利用 Flow 操作符的声明式优势。改成下面这样会清晰很多:
// ✅ 声明式:用操作符描述逻辑
viewModel.data
.catch { emit(Result.Error(it.message ?: "unknown")) }
.onEach { result ->
when (result) {
is Success -> updateUI(result.data)
is Error -> showError(result.msg)
}
}
.launchIn(lifecycleScope)
💡 我的建议:开始学习 Flow 之前,先花十分钟理解数据流 → 操作符 → 订阅者这个核心三角。后面所有的技巧都是在这个框架上变花样。
本章小结(关键知识点回顾)
| 概念 | 一句话记法 |
|---|---|
| Flow | 异步数据流,冷、挂起、背压 |
| 响应式编程 | 数据主动推送,你只需要声明「当…时…」 |
| 与 RxJava 区别 | Flow 更轻量、自动取消、挂起式背压 |
| 学习重点 | 生产者 → 操作符 → 消费者 这条链 |
我个人的体会是:Flow 其实没那么玄乎,它就是把异步回调包装成了一条你有控制权的管道。掌握好这个核心形象,后面学的所有操作符都只是在这条管子上加阀门、分叉、合并而已。