4. Kotlin协程进阶:Flow、StateFlow、SharedFlow与Channel
协程的基础用法大家应该都熟悉了。但真正到了生产环境,你会发现光有launch和async远远不够。数据流怎么处理?状态怎么共享?组件间怎么通信?
嗯,这就是我们今天要聊的内容。我个人觉得,这几个工具是区分「会用协程」和「用好协程」的分水岭。
4.1 Flow:冷数据流的正确姿势
先说说Flow。它本质上是一个冷数据流——什么意思?就是没有消费者的时候,它不会生产数据。这跟RxJava的Observable有点像,但更轻量,而且完全挂起式。
我在项目中遇到过这样一个场景:用户打开一个商品详情页,需要从三个接口拉数据——商品信息、评价列表、推荐商品。用Flow来编排,代码会非常干净。
// 商品详情页的数据流
fun loadProductDetail(productId: String): Flow<ProductDetailUiState> = flow {
// 先展示骨架屏
emit(ProductDetailUiState.Loading)
// 并行请求三个接口
val productInfo = async { repository.getProductInfo(productId) }
val reviews = async { repository.getReviews(productId) }
val recommendations = async { repository.getRecommendations(productId) }
// 合并结果
emit(ProductDetailUiState.Success(
info = productInfo.await(),
reviews = reviews.await(),
recommendations = recommendations.await()
))
}.catch { e ->
emit(ProductDetailUiState.Error(e.message ?: "未知错误"))
}.flowOn(Dispatchers.IO) // 指定上游在IO线程执行
这里有个细节要注意:flowOn只改变上游的上下文,下游的collect还是在原来的协程上下文里。说白了,你可以在IO线程拉数据,在主线程更新UI。
flowOn(Dispatchers.IO)后面。这样UI层永远不用关心线程切换的问题。
4.2 StateFlow:状态持有者的最佳选择
StateFlow,你可以把它理解成「可观察的状态容器」。它跟LiveData最大的区别是什么?
StateFlow需要初始值,而且它是无粘性的——等等,这话不准确。准确地说,StateFlow默认会重放最新值,但你可以通过WhileSubscribed策略来控制。
class UserViewModel : ViewModel() {
// 私有化MutableStateFlow,对外暴露不可变的StateFlow
private val _uiState = MutableStateFlow(UserUiState())
val uiState: StateFlow<UserUiState> = _uiState.asStateFlow()
fun updateUserName(name: String) {
_uiState.update { it.copy(userName = name) }
}
}
我曾经踩过一个坑:在Fragment的onViewCreated里直接collect StateFlow,结果每次屏幕旋转都会重新触发网络请求。为什么?因为StateFlow在Lifecycle.State.STARTED以下时,collect会挂起,但上游的flow还在跑。
lifecycleScope.launch去collect StateFlow,结果页面不可见时数据还在更新UI,直接Crash。后来改用repeatOnLifecycle(Lifecycle.State.STARTED)才解决。
// 正确的做法:在Lifecycle范围内收集
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
// 更新UI
}
}
}
4.3 SharedFlow:事件总线的现代替代品
SharedFlow跟StateFlow最大的不同:它可以没有初始值,而且可以配置重放策略。说白了,StateFlow是「状态」,SharedFlow是「事件」。
你想想看,如果用户点击了一个按钮,需要弹出一个Snackbar。这个「弹出」是一个事件,不是状态。用StateFlow来存?不合适,因为屏幕旋转后状态还在,Snackbar会再次弹出。
class EventViewModel : ViewModel() {
// SharedFlow配置:不重放,不缓存
private val _events = MutableSharedFlow<UiEvent>(
replay = 0,
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
val events: SharedFlow<UiEvent> = _events.asSharedFlow()
fun onButtonClick() {
viewModelScope.launch {
_events.emit(UiEvent.ShowSnackbar("操作成功"))
}
}
}
这里有个参数要特别注意:extraBufferCapacity。我刚开始用的时候没设这个值,结果emit的时候抛异常了——因为默认的SharedFlow没有缓冲区,emit会挂起直到有人collect。设成1之后,即使没有订阅者,事件也能发出去。
| 特性 | StateFlow | SharedFlow |
|---|---|---|
| 初始值 | 必须 | 可选 |
| 重放 | 总是重放最新值 | 可配置(replay参数) |
| 典型用途 | UI状态 | 一次性事件 |
| 背压处理 | 无(总是最新值) | 可配置溢出策略 |
4.4 Channel:协程间的管道通信
Channel,说白了就是协程版的BlockingQueue。一个协程往里放数据,另一个协程往外取。但它是挂起式的,不会阻塞线程。
我在项目中用Channel做过一个日志收集器:多个协程同时产生日志,一个消费者协程负责写入文件。
// 日志收集器示例
class LogCollector {
private val logChannel = Channel<LogEntry>(Channel.BUFFERED)
suspend fun log(entry: LogEntry) {
logChannel.send(entry)
}
fun startCollecting(scope: CoroutineScope) {
scope.launch(Dispatchers.IO) {
for (entry in logChannel) {
// 写入文件
writeToFile(entry)
}
}
}
}
Channel有几种类型:
- RendezvousChannel:默认类型,没有缓冲区。send和receive必须同时准备好,否则挂起。
- BufferedChannel:有缓冲区,满了之后send挂起。
- ConflatedChannel:只保留最新值,旧值被覆盖。跟StateFlow有点像。
- UnlimitedChannel:无限制缓冲区,小心OOM。
- Flow:冷数据流,适合异步数据序列
- StateFlow:热数据流,适合UI状态
- SharedFlow:热数据流,适合事件
- Channel:协程间通信,适合生产者-消费者模式
4.5 实战选择指南
说了这么多,到底什么时候用哪个?我给大家一个简单的判断标准:
- 数据是「状态」还是「事件」?状态用StateFlow,事件用SharedFlow。
- 数据是「冷」还是「热」?冷数据(如数据库查询)用Flow,热数据(如UI状态)用StateFlow。
- 需要点对点通信?用Channel。比如一个协程生产数据,一个协程消费数据。
- 需要多个订阅者?用SharedFlow。Channel是一对一的。
我个人习惯是:ViewModel里对外暴露StateFlow给UI层,内部用SharedFlow处理一次性事件。跨模块通信用Channel,但要注意生命周期管理。
嗯,最后说一句:这些工具没有银弹。你用得越多,越能体会到它们各自的适用场景。刚开始可能会选错,没关系,重构成本很低——因为它们的API设计都很相似。