第一章 协程与 Flow 概述
1.1 发展背景
协程这个概念早在 60 年代就出现了,但真正在 Android 圈火起来,是 Kotlin 在 1.3 版本把它带进来的。我记得 2019 年第一次用协程时,心里还在嘀咕:这不就是线程换了个马甲吗?
后来仔细一琢磨,还真不一样。协程是跑在用户态的轻量级线程,由编译器帮我们管理挂起和恢复。说白了,你写同步的代码,跑出异步的效果。不用再回调地狱里修修补补了。
Flow 呢?它是 Kotlin 协程生态里专门处理冷数据流的工具。我用 RxJava 好几年,看到 Flow 的第一反应:这不就是 RxJava 的 Observable 吗?但用下来发现,Flow 天生跟协程绑定,结构化并发,取消自动,测试起来也清爽很多。
| 对比维度 | 传统回调 | RxJava | Kotlin 协程 + Flow |
|---|---|---|---|
| 代码可读性 | 嵌套多,难维护 | 链式调用,稍好 | 同步风格,自然 |
| 取消机制 | 手动处理 | Disposable 管理 | 结构化取消,自动 |
| 背压支持 | 无 | 内置策略 | Flow 自带缓冲/合并 |
| 测试难度 | Mock 回调复杂 | TestScheduler 还行 | runTest + 虚拟时间,爽 |
1.2 核心概念
我们先看协程的两个核心:挂起函数和协程作用域。
suspend 关键字,它可以在不阻塞线程的前提下挂起当前协程,等结果回来再恢复。你想想看,它不像线程那样阻塞,而是让出执行权,线程继续干别的活。
协程作用域则决定了协程的生命周期。我个人习惯把作用域绑定到 ViewModel 或 Activity 上,比如 viewModelScope。页面销毁,协程自动取消,不会泄露。
什么是 Flow?
Flow 是一个冷流:只有被收集(collect)时才开始发射数据。跟 LiveData 不一样,它默认不在主线程发射,需要我们用 flowOn 指定调度器。
// 一个简单的 Flow 示例
fun fetchUserData(): Flow<String> = flow {
for (i in 1..3) {
delay(1000)
emit("User data chunk $i")
}
}
嗯,这里要注意:Flow 里的 emit 是挂起函数,不会阻塞线程。我在项目中遇到过,有人把 emit 放到 withContext(Dispatchers.IO) 里,忘了 flowOn,结果数据全在主线程发射了……
1.3 适用场景归纳总结
协程跟 Flow 并不是银弹,但有几个场景用起来是真香:
- 网络请求串行/并行:用
async/await或flatMapConcat,比回调美观太多。 - 数据流观测:比如定位变化、传感器数据,用
callbackFlow转成 Flow,再配合catch处理异常。 - UI 层刷新:Flow 的
collectLatest可以跳过中间值,只处理最新数据。我用它做搜索防抖,省了自定义 debounce 的代码。 - 单元测试:协程的
runTest加上TestDispatcher,能模拟时间快进。我曾经因为异步测试跑得慢,后来全换成虚拟时间,测试套件从 5 分钟缩到 30 秒。
viewModelScope.launch 配合 suspend 函数就够了。只有当你需要连续发射多个值,或者要做数据变换时,才上 Flow。别把 Flow 当成万能工具,它是有代价的(冷流本质,多次 collect 会重复执行)。
StateFlow 或者 shareIn 做热流转换。
1.4 总结
协程和 Flow 并不是什么新魔法,它们只是帮我们把异步代码的复杂度藏到了语言层面。我个人觉得,理解「挂起」的本质(不是阻塞,而是让出线程)比背 API 更重要。
至于适用场景,记住三点就够了:
- 一次请求 →
suspend函数 - 多次发射 →
Flow - 状态共享 →
StateFlow或SharedFlow
好了,这章先聊到这儿。接下来在实践环节,我们再一个一个拆开细看。