82、Kotlin协程进阶:Flow、StateFlow、SharedFlow、Channel、协程异常处理

协程基础大家都会了,但真正到项目里,你会发现光有 launch 和 async 根本不够用。数据流怎么监听?状态怎么共享?异常崩了怎么办?

这一讲,我把这几个进阶知识点串起来讲。都是我在实际项目中踩过坑、重构过多次的东西。你耐心看完,绝对能少走弯路。

一、Flow:冷流,数据生产者的优雅方案

Flow 说白了就是协程版的序列。它是个冷流——什么意思?没人 collect,它就不干活。

// 一个简单的 Flow
fun getNews(): Flow<News> = flow {
    repeat(5) {
        delay(1000)
        emit(fetchNewsFromApi()) // 发射数据
    }
}

// 消费
lifecycleScope.launch {
    getNews().collect { news ->
        tvNews.text = news.title
    }
}

我在项目中遇到过一个问题:用 LiveData 监听数据库变化,结果每次旋转屏幕都重新查询。换成 Flow + Room 后,这个问题自然解决了。因为 Flow 是冷流,只有被 collect 时才触发查询。

我的习惯:数据层统一用 Flow 暴露数据。Room、Retrofit 原生支持 Flow,配合起来非常丝滑。

二、StateFlow:热流,状态持有者

StateFlow 是 Flow 的升级版。它是个热流,始终持有最新值。而且它只会在值变化时才通知订阅者。

class MainViewModel : ViewModel() {
    // 对外暴露不可变的 StateFlow
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()

    fun updateName(name: String) {
        _uiState.update { it.copy(userName = name) }
    }
}

// Activity 中收集
lifecycleScope.launch {
    viewModel.uiState
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect { state ->
            // 更新 UI
        }
}

为什么要用 flowWithLifecycle?因为 StateFlow 是热流,不配合 Lifecycle 的话,即使界面不可见,它还在发射数据。我刚开始没加这个,结果后台时还在更新 UI,白白浪费性能。

注意:StateFlow 的初始值必须给。它不像 LiveData 可以为 null。你想想看,如果初始状态不确定,可以用 sealed class 包装一下。

三、SharedFlow:热流,事件总线替代品

SharedFlow 比 StateFlow 更灵活。它不持有状态,而是广播事件。适合做一次性事件,比如 Snackbar 提示、导航跳转。

class EventBus {
    private val _events = MutableSharedFlow<UiEvent>()
    val events: SharedFlow<UiEvent> = _events.asSharedFlow()

    suspend fun sendEvent(event: UiEvent) {
        _events.emit(event)
    }
}

// 使用
lifecycleScope.launch {
    eventBus.events
        .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
        .collect { event ->
            when (event) {
                is UiEvent.ShowSnackbar -> showSnackbar(event.message)
                is UiEvent.Navigate -> findNavController().navigate(event.route)
            }
        }
}

SharedFlow 有几个参数要留意:replay(重放给新订阅者数量)、extraBufferCapacity(缓冲区大小)。

我踩过一个坑:SharedFlow 默认没有缓冲区,如果 emit 时没有订阅者,事件就丢了。后来我设了 replay = 1,才解决了这个问题。

特性 StateFlow SharedFlow
是否持有状态 是,始终有最新值 否,只广播事件
初始值 必须提供 不需要
去重 自动去重(值不变不通知) 默认不去重
典型用途 UI 状态 一次性事件

四、Channel:协程间的管道

Channel 是协程版的 BlockingQueue。一个协程往里放,另一个协程往外取。它支持背压——如果消费者处理不过来,生产者会被挂起。

val channel = Channel<String>(capacity = Channel.BUFFERED)

// 生产者
lifecycleScope.launch {
    for (i in 1..10) {
        channel.send("消息 $i")
        delay(100)
    }
    channel.close() // 记得关闭
}

// 消费者
lifecycleScope.launch {
    for (msg in channel) {
        delay(500) // 模拟慢消费
        Log.d("TAG", "收到: $msg")
    }
}

Channel 有几种类型:Rendezvous(无缓冲区,必须配对)、Buffered(有缓冲区)、Conflated(只保留最新值)、Unlimited(无限缓冲区)。

我个人习惯用 Buffered,容量设 64 或 128。太大容易 OOM,太小容易挂起生产者。

核心区别:Channel 是一对一的管道,SharedFlow 是一对多的广播。如果你只需要一个生产者一个消费者,用 Channel 更轻量。

五、协程异常处理

协程的异常处理,说实话,刚开始挺让人头疼的。因为它的传播机制和普通 try-catch 不太一样。

5.1 异常传播规则

  • launch:异常会向上传播到父协程,导致父协程取消
  • async:异常在 await() 时抛出,不会自动传播
  • SupervisorJob:子协程异常不会影响兄弟协程
// launch 异常会取消父协程
lifecycleScope.launch {
    launch {
        throw RuntimeException("子协程挂了")
    }
    launch {
        delay(1000)
        Log.d("TAG", "这个不会执行")
    }
}

// 用 SupervisorJob 隔离异常
lifecycleScope.launch(SupervisorJob()) {
    launch {
        throw RuntimeException("子协程挂了")
    }
    launch {
        delay(1000)
        Log.d("TAG", "这个会正常执行")
    }
}

5.2 全局异常处理

可以用 CoroutineExceptionHandler 捕获未处理的异常。注意,它只对 launch 有效,async 的异常还是要靠 try-catch。

val handler = CoroutineExceptionHandler { _, throwable ->
    Log.e("TAG", "捕获到异常: ${throwable.message}")
}

lifecycleScope.launch(handler) {
    throw RuntimeException("测试异常")
}

5.3 Flow 中的异常处理

Flow 提供了 catch 操作符,专门处理上游异常。

flow {
    emit(getData())
    throw RuntimeException("出错了")
}.catch { e ->
    emit(fallbackData) // 发射备用数据
    Log.e("TAG", "Flow 异常: ${e.message}")
}.collect { data ->
    // 正常处理
}
我曾经踩过的坑:在 collect 里用 try-catch 捕获异常,结果发现 catch 块里的代码执行了,但 Flow 还是停了。后来才明白,catch 操作符才是正确姿势。try-catch 只能捕获 collect 块的异常,不能恢复 Flow 的执行。

六、知识体系总览

下面这张图,是我梳理的协程进阶知识结构。你看完应该能有个整体印象。

Kotlin 协程进阶 Flow(冷流) 数据生产,没人 collect 不执行 StateFlow(热流) 持有状态,自动去重 SharedFlow(热流) 广播事件,替代 EventBus Channel(管道) 协程间一对一双向通信 异常处理 launch/async/SupervisorJob 💡 选型建议 UI 状态 → StateFlow 一次性事件 → SharedFlow 数据生产 → Flow 协程间通信 → Channel

七、实战建议

说了这么多,我总结几条实战经验:

  1. 数据层用 Flow:Room、Retrofit 原生支持,配合 map、filter 等操作符非常方便
  2. ViewModel 用 StateFlow:替代 LiveData,天然支持协程,配合 flowWithLifecycle 更安全
  3. 事件用 SharedFlow:Snackbar、导航、弹窗,设 replay = 1 防止丢事件
  4. 异常用 SupervisorJob:一个子协程挂了,不影响其他兄弟协程
  5. Channel 别滥用:大多数场景 SharedFlow 就够了,Channel 留给需要背压的场景
我的一个小技巧:写单元测试时,用 TestCoroutineDispatcher 控制时间。Flow 的测试用 Turbine 库,非常方便。你想想看,如果不用这些工具,测试协程代码有多痛苦。

好了,这一讲的内容就这些。协程进阶其实不难,关键是理解冷流和热流的区别,以及异常传播的规则。把这些搞清楚了,项目里用起来就顺手多了。

公众号:蓝海资料掘金营,微信deep3321