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 { emit() } 中间操作 map · filter · catch 消费者 collect { } 背压管理 挂起等待消费者 冷流特性 有订阅才生产 结构化并发 自动取消 🎯 核心要点 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 其实没那么玄乎,它就是把异步回调包装成了一条你有控制权的管道。掌握好这个核心形象,后面学的所有操作符都只是在这条管子上加阀门、分叉、合并而已。