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 时才触发查询。
二、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,白白浪费性能。
三、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,太小容易挂起生产者。
五、协程异常处理
协程的异常处理,说实话,刚开始挺让人头疼的。因为它的传播机制和普通 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 ->
// 正常处理
}
六、知识体系总览
下面这张图,是我梳理的协程进阶知识结构。你看完应该能有个整体印象。
七、实战建议
说了这么多,我总结几条实战经验:
- 数据层用 Flow:Room、Retrofit 原生支持,配合 map、filter 等操作符非常方便
- ViewModel 用 StateFlow:替代 LiveData,天然支持协程,配合 flowWithLifecycle 更安全
- 事件用 SharedFlow:Snackbar、导航、弹窗,设 replay = 1 防止丢事件
- 异常用 SupervisorJob:一个子协程挂了,不影响其他兄弟协程
- Channel 别滥用:大多数场景 SharedFlow 就够了,Channel 留给需要背压的场景
好了,这一讲的内容就这些。协程进阶其实不难,关键是理解冷流和热流的区别,以及异常传播的规则。把这些搞清楚了,项目里用起来就顺手多了。