首页 / Android 入门教程 / Kotlin 协程进阶

Android 入门教程

Kotlin 协程进阶

本教程共 100 篇 · 第 74 篇 · 更新于 2026-07-28 · 约 10 分钟阅读

AndroidAndroid 入门教程Kotlin协程FlowStateFlowSharedFlow

74. Kotlin 协程进阶

本节目标:掌握协程进阶用法,理解 Flow 冷流和热流的区别,会用 StateFlow/SharedFlow 替代 LiveData,知道协程异常怎么传播、怎么用 SupervisorJob 隔离故障,写出结构清晰、稳定可取消的异步代码。

前面 Kotlin 章节讲过协程基础(launchasyncDispatcherswithContext)。这章讲实战中真正常用的进阶部分: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:切换上游时取消旧的,搜词联想场景常用。
Tip

Room 的 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("出错了")
}
Note

StateFlow 的相等判断基于 equals,连续 value = 同值不会触发更新。要强制刷新用 MutableStateFlowupdate 或者直接 value = value.copy()

SharedFlow:事件广播

SharedFlow 也是热流,但更通用的「事件流」用途。和 StateFlow 区别:

维度StateFlowSharedFlow
初始值必须有可没有
最新值永远保留可配置保留多少
用途状态持有事件广播
重复值自动去重默认不去重

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

冷流转热流

冷流转热流用 stateInshareIn

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,就是反模式,任务没法管理,容易泄漏。

常见坑

  1. GlobalScope 启动协程:生命周期不可控,应用崩了协程还在跑。用 viewModelScopelifecycleScope
  2. StateFlow 装一次性事件:用户切回来重复触发。
  3. Flow 忘了 flowOn:上游在主线程跑,卡 UI。
  4. async 不 await:异常不会抛出,被吞。
  5. 取消后 finally 里调挂起函数:挂起失败。用 NonCancellable
  6. SupervisorJob 滥用:隔离过度,异常被吞没察觉。

小结

协程进阶主要两件事:Flow 处理数据流,SupervisorJob + CoroutineExceptionHandler 处理异常。StateFlow 替代 LiveData 做状态容器,SharedFlow 做一次性事件。结构化并发是底座,scope 选对了一切都顺。

下一章讲 WorkManager,把「即使应用关闭也要执行」的任务搞定。