26、Flow与跨组件通信:SharedFlow的跨组件通信、事件总线替代方案、作用域管理
跨组件通信,说白了就是让两个八竿子打不着的模块能对上话。在Android里,这事以前靠EventBus、RxBus,后来靠LiveData,现在嘛——我个人习惯用SharedFlow。为什么?因为它既有Flow的冷流特性,又能变成热流,天然适合做事件总线。
我接手过一个老项目,里面EventBus满天飞,光看注册反注册的代码就头疼。后来重构时全换成了SharedFlow,清爽多了。今天我们就聊聊怎么用SharedFlow做跨组件通信,以及那些你容易踩的坑。
SharedFlow的核心概念
SharedFlow是一种热流。什么意思?它不依赖收集者存在,数据发出去后,后来的收集者也能收到——前提是你配置了合适的策略。
先看个最简单的例子:
class EventBus {
private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()
suspend fun emit(event: Event) {
_events.emit(event)
}
}
嗯,就这么简单。但实际用起来,有几个参数你得搞明白。
SharedFlow的三个关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
| replay | 0 | 新订阅者能收到之前发出的N个事件 |
| extraBufferCapacity | 0 | 缓冲区大小,防止emit挂起 |
| onBufferOverflow | BufferOverflow.SUSPEND | 缓冲区满时的策略:挂起、丢弃旧数据、丢弃新数据 |
我项目中常用的配置是:
val sharedFlow = MutableSharedFlow<Event>(
replay = 1,
extraBufferCapacity = 2,
onBufferOverflow = BufferOverflow.DROP_OLDEST
)
为什么replay设成1?因为有些场景下,比如用户登录成功后发了个事件,页面刚打开还没收集到,设成1就能让新页面收到最近一次事件。说白了就是「保底」。
事件总线替代方案
传统的EventBus有个问题——你永远不知道谁订阅了谁,调试起来像大海捞针。SharedFlow方案就好很多,因为它是类型安全的,而且作用域可控。
我一般会封装一个更完善的版本:
object AppEventBus {
private val events = mutableMapOf<Class<*>, MutableSharedFlow<Any>>()
@Suppress("UNCHECKED_CAST")
fun <T : Any> observe(eventType: Class<T>): SharedFlow<T> {
return events.getOrPut(eventType) {
MutableSharedFlow(replay = 0, extraBufferCapacity = 1)
} as SharedFlow<T>
}
suspend fun <T : Any> emit(event: T) {
val flow = events[event::class.java] ?: return
(flow as MutableSharedFlow<T>).emit(event)
}
}
这样用起来就很直观了:
// 发送方
lifecycleScope.launch {
AppEventBus.emit(UserLoggedInEvent(userId = "123"))
}
// 接收方
lifecycleScope.launch {
AppEventBus.observe(UserLoggedInEvent::class.java).collect { event ->
// 处理登录事件
}
}
你想想看,这比EventBus的字符串标记强多了吧?编译期就能检查类型,重构时也不怕改漏了。
作用域管理——最容易翻车的地方
我曾经在一个项目里看到这样的代码:
// 错误示范
GlobalScope.launch {
eventBus.events.collect { event ->
// 处理事件
}
}
GlobalScope!这玩意一用,协程就跟应用同生共死了。页面销毁了,协程还在跑,事件还在收,然后你更新UI时发现View已经没了——直接崩溃。
正确的做法是用lifecycleScope或viewModelScope:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
lifecycleScope.launch {
eventBus.events.collect { event ->
// 安全地更新UI
}
}
}
}
但这里有个细节——如果你在Fragment里收集,记得用repeatOnLifecycle:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
eventBus.events.collect { event ->
// 只在STARTED及以上状态时收集
}
}
}
为什么?因为如果不加这个,即使页面在后台,事件也会被收集。你想想看,用户在别的页面操作,事件发过来,你的Fragment在后台还在处理——浪费资源不说,还可能出问题。
避坑指南
- 用tryEmit代替emit时,如果缓冲区满了,事件会静默丢失。我有个bug查了两天,最后发现是emit太快,DROP_OLDEST把关键事件丢了。
- SharedFlow的collect是挂起函数,不要在非协程环境调用。有人用runBlocking包了一层,结果主线程卡死。
- 多个订阅者共享同一个SharedFlow时,如果其中一个cancel了,其他不受影响。但如果你用StateFlow,它只保留最新值,不适合做事件总线。
知识体系总览
下面这张图帮你理清SharedFlow在跨组件通信中的位置:
总结一下
SharedFlow做跨组件通信,说白了就是三个要点:
- 参数配置要合理——replay、缓冲区、溢出策略,根据场景选
- 作用域要绑定生命周期——别用GlobalScope,用lifecycleScope或viewModelScope
- 类型安全是底线——别用字符串标记事件,用具体类
我见过太多人把SharedFlow当EventBus用,结果忘了配参数,事件丢了都不知道。嗯,这些坑我都踩过,希望你别再踩一遍。
公众号:蓝海资料掘金营,微信deep3321