Kotlin 协程进阶
本教程共 100 篇 · 第 74 篇 · 更新于 2026-07-28 · 约 10 分钟阅读
74. Kotlin 协程进阶
本节目标:掌握协程进阶用法,理解 Flow 冷流和热流的区别,会用 StateFlow/SharedFlow 替代 LiveData,知道协程异常怎么传播、怎么用 SupervisorJob 隔离故障,写出结构清晰、稳定可取消的异步代码。
前面 Kotlin 章节讲过协程基础(launch、async、Dispatchers、withContext)。这章讲实战中真正常用的进阶部分:Flow 和异常处理。
为什么需要 Flow
协程的 suspend 函数只能返回一个值。但很多场景是「数据流」:监听数据库变化、监听位置更新、监听 WebSocket 消息—一次返回不够,要持续接收。
打个比方,suspend 函数像点外卖,下单等送达;Flow 像订牛奶,每天送一瓶,持续供应。
// 一次性
suspend fun getUser(): User
// 持续观察
fun observeUser(): Flow<User>
Flow 是冷流
Flow 是「冷流」,没有观察者就不会真正执行。每次 collect 才会触发上游逻辑。
fun numbers(): Flow<Int> = flow {
for (i in 1..3) {
delay(500)
emit(i)
}
}
// 不 collect 不执行
numbers()
// collect 才开始 emit
numbers().collect { println(it) }
// 输出:1(500ms 后)、2(再 500ms)、3(再 500ms)
冷流的好处:资源按需分配,没人观察就不消耗。
Flow 操作符
Flow 支持一堆类似集合的操作符:
fun userStream(): Flow<User> = ...
userStream()
.map { it.name } // 转换
.filter { it.isNotEmpty() } // 过滤
.distinctUntilChanged() // 去重
.debounce(300) // 防抖
.flowOn(Dispatchers.IO) // 切换上游线程
.catch { e -> emit(fallback()) } // 捕获异常
.collect { name -> show(name) }
常用操作符:
map/filter/reduce:和集合一样。flowOn:指定上游执行调度器,不影响下游。catch:捕获上游异常,可以发兜底数据。debounce:防抖,连续事件只保留最后一个。combine/zip:合并多个 Flow。flatMapLatest:切换上游时取消旧的,搜词联想场景常用。
TipRoom 的 DAO 方法返回
Flow<List<T>>时,数据库一变 Flow 自动发新值,配合distinctUntilChanged性能更好。
StateFlow:热流的状态容器
StateFlow 是「热流」,永远有值(有初始值),多个观察者共享同一份数据。它就是用来替代 LiveData 的。
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun load() {
viewModelScope.launch {
_uiState.value = UiState.Loading
try {
val data = repo.fetch()
_uiState.value = UiState.Success(data)
} catch (e: Exception) {
_uiState.value = UiState.Error(e)
}
}
}
}
特点:
- 永远有当前值,新观察者订阅立即收到最新值。
value属性可读可写,像普通变量。conflate默认开启,新值覆盖旧值,观察者只收到最新。- 多个观察者共享上游。
在 Compose 里用:
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
when (uiState) {
is UiState.Loading -> CircularProgressIndicator()
is UiState.Success -> Text((uiState as UiState.Success).data)
is UiState.Error -> Text("出错了")
}
NoteStateFlow 的相等判断基于
equals,连续value =同值不会触发更新。要强制刷新用MutableStateFlow的update或者直接value = value.copy()。
SharedFlow:事件广播
SharedFlow 也是热流,但更通用的「事件流」用途。和 StateFlow 区别:
| 维度 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须有 | 可没有 |
| 最新值 | 永远保留 | 可配置保留多少 |
| 用途 | 状态持有 | 事件广播 |
| 重复值 | 自动去重 | 默认不去重 |
SharedFlow 适合「一次性事件」,比如显示 Snackbar、跳转页面:
class MyViewModel : ViewModel() {
private val _events = MutableSharedFlow<String>()
val events = _events.asSharedFlow()
fun save() {
viewModelScope.launch {
// ... 保存逻辑
_events.emit("保存成功")
}
}
}
观察:
LaunchedEffect(Unit) {
viewModel.events.collect { msg ->
scaffoldState.snackbarHostState.showSnackbar(msg)
}
}
Warning一次性事件别用 StateFlow。StateFlow 会保留最新值,新观察者订阅时会重新收到上次的「保存成功」,用户切回来又弹一次 Snackbar,体验差。用 SharedFlow 配合
replay = 0。
冷流转热流
冷流转热流用 stateIn 或 shareIn:
val users: StateFlow<List<User>> = userDao.getAll()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = emptyList()
)
WhileSubscribed(5000):有观察者才启动上游,最后一个观察者离开 5 秒后停止。这种策略最省资源。Eagerly:立即启动,不管有没有观察者。Lazily:第一个观察者来了就启动,永不停。
协程异常传播
协程的异常传播有讲究。launch 启动的协程抛异常会传到父 Job,父 Job 取消所有子协程。
val scope = CoroutineScope(Job())
scope.launch {
launch {
delay(100)
throw RuntimeException("子协程挂了")
}
launch {
delay(200)
println("我也跑不到了")
}
}
第一个子协程抛异常,整个 scope 被取消,第二个子协程也跟着死。这叫「结构化并发」。
SupervisorJob 隔离故障
不希望一个挂全挂,用 SupervisorJob 或者 supervisorScope:
val scope = CoroutineScope(SupervisorJob())
scope.launch {
throw RuntimeException("挂了")
}
scope.launch {
delay(200)
println("我还在跑") // 依然能执行
}
或者局部用:
supervisorScope {
launch { throw RuntimeException("挂了") }
launch { println("我没事") }
}
Tip多个子任务互相独立时(比如并行加载多张图片)用
supervisorScope,一张失败不影响其他。
CoroutineExceptionHandler
不想让异常崩应用,装个全局处理器:
val handler = CoroutineExceptionHandler { _, e ->
Log.e("App", "协程异常", e)
reportToAnalytics(e)
}
scope.launch(handler) {
throw RuntimeException("oops")
}
注意几点:
- 只对
launch生效,async的异常会通过await()抛出。 - 只能装在顶层协程,子协程装了无效。
- 不处理就崩,处理了就走 handler。
withContext 切线程
suspend 函数要保证「主线程安全」—主线程调用不阻塞。用 withContext 切到 IO:
suspend fun readFile(path: String): String = withContext(Dispatchers.IO) {
File(path).readText()
}
withContext 会挂起当前协程,切到指定调度器执行,结束后切回原调度器。
Note不要在
suspend函数里用GlobalScope.launch切线程。那是开了新协程,原协程还在等。用withContext是同协程切线程。
协程取消
协程的取消是协作式的,调用 cancel() 只是发个信号,协程要在合适的点检查取消状态。
val job = scope.launch {
repeat(1000) { i ->
ensureActive() // 检查取消状态,已取消就抛 CancellationException
println(i)
}
}
delay(100)
job.cancel()
挂起函数(delay、网络请求的 suspend 方法)会自动检查取消,不用手动处理。但 CPU 密集型循环要自己加 ensureActive() 或 yield()。
Warning用
isActive判断取消状态但不抛异常,循环里if (!isActive) return也行。但要小心 finally 块里的挂起函数—取消中的协程不能调delay之类的,要用withContext(NonCancellable) { ... }。
结构化并发
结构化并发是协程的核心思想:子协程不能比父协程活得更久。好处:
- 自动取消:父取消,所有子跟着取消。
- 异常传播:子异常会传到父,不会丢。
- 作用域清晰:协程都在某个 scope 里,不会乱跑。
坏处也明显:忘加 scope 用 GlobalScope,就是反模式,任务没法管理,容易泄漏。
常见坑
- GlobalScope 启动协程:生命周期不可控,应用崩了协程还在跑。用
viewModelScope、lifecycleScope。 - StateFlow 装一次性事件:用户切回来重复触发。
- Flow 忘了
flowOn:上游在主线程跑,卡 UI。 - async 不 await:异常不会抛出,被吞。
- 取消后 finally 里调挂起函数:挂起失败。用
NonCancellable。 - SupervisorJob 滥用:隔离过度,异常被吞没察觉。
小结
协程进阶主要两件事:Flow 处理数据流,SupervisorJob + CoroutineExceptionHandler 处理异常。StateFlow 替代 LiveData 做状态容器,SharedFlow 做一次性事件。结构化并发是底座,scope 选对了一切都顺。
下一章讲 WorkManager,把「即使应用关闭也要执行」的任务搞定。