在 Android 架构演进的历史长河中,以 EventBus、RxBus 为代表的"全局事件总线"曾是一代神器。它一举击碎了传统 Android 嵌套回调与复杂 Interface 传值的繁琐,让跨页面通信变得轻而易举。 之前写过:
Android 现代架构不需要事件总线进阶篇 (如果你没有看过,可以先看看这两篇文章)
然而,随着 Kotlin Coroutines 和 Flow 的全面普及,不少团队在重构项目时,第一件事就是用 MutableSharedFlow 封装一个所谓的 FlowBus:
kotlin
// 许多项目中常见的 FlowBus 封装
object FlowBus {
private val bus = ConcurrentHashMap<String, MutableSharedFlow<Any>>()
fun <T> with(key: String): MutableSharedFlow<T> {
return bus.getOrPut(key) { MutableSharedFlow() } as MutableSharedFlow<T>
}
}
将传统的 EventBus 替换为基于 Flow 的实现,表面上看是用上了现代的协程 API,但在架构设计思想上,却是一次典型的"开倒车"。
在强调边界、责任与单向数据流的现代架构里塞进一个全局事件总线,就像在民主国家保留一个可以随时下旨的皇帝:制度表面仍在运转,但关键决策可以绕过既定流程,最终没有人能说清权力从哪里来、又流向哪里。
毁掉一个好端端的现代架构,往往只需要这几行自定义的 FlowBus 代码。
一、 为什么说用 FlowBus 是在毁掉现代架构?
1. 换了 API,却没有换设计思想
决定架构先进性的不是所使用的工具(Flow vs Handler/RxJava),而是数据流向与状态管理的设计模式。
全局事件总线的本质是隐式多对多通信 。任何组件都可以随时向总线发送事件,任何组件也都可以监听总线。用 SharedFlow 重构后的 FlowBus,依然保留了所有的历史隐式弊端:
- 数据流向混乱:系统内部呈现网状的交叉依赖,极难画出清晰的数据流走向图。
- 排查成本极高 :当 UI 端接收到异常数据时,你无法通过常规的"查找引用(Find Usages)"精准定位是哪里的代码在什么时机
emit了这个事件。
2. 彻底击碎了"单向数据流(UDF)"
现代 Android 架构(MVI / MVVM + Jetpack)的核心命脉是 单向数据流(Unidirectional Data Flow)。
在标准的 UDF 架构中: 用户操作 (Intent) -> ViewModel 改变 State -> UI 响应 State 刷新
而事件总线打破了这个闭环。它允许 UI 或业务层绕过 ViewModel 的状态管理,直接通过全局广播触发其他组件的变更,导致 UI 变成了"事件驱动"而非"状态驱动"。
二、 场景重构:现代架构如何彻底替换 EventBus?
以下针对开发中最常滥用 EventBus 的三大典型场景,给出标准的现代架构演进方案。
场景 1:跨页面/全局状态更新(如:用户登录状态变化)
开倒车的做法(FlowBus 广播通知)
发送方只管抛出事件,接收方收到通知后再各自去处理逻辑,缺乏统一的数据权威来源。
kotlin
// 1. 发送事件
suspend fun onLoginSuccess(user: User) {
FlowBus.with<UserEvent>("login").emit(UserEvent.LoggedIn(user))
}
// 2. 接收方(如 ProfileFragment)
lifecycleScope.launch {
FlowBus.with<UserEvent>("login").collect { event ->
updateUserProfile(event.user)
}
}
现代架构做法:单一数据源(SSOT)+ 冷流 Flow
数据存储在唯一的 Repository 中,**数据层仅暴露只读的冷流 Flow**(保证资源的按需订阅与自动释放,避免在数据层偷跑 ViewModel 的热流持有逻辑),由 ViewModel 负责将其转换为热流 StateFlow 驱动 UI。
kotlin
// 1. 数据层(Repository 单例):暴露冷流 Flow,不主动持有 State
@Singleton
class UserRepository @Inject constructor(
private val userDao: UserDao
) {
// 纯粹的响应式数据源,ViewModel 不 collect 它就不自动触发,避免占用后台资源
fun observeUserSession(): Flow<UserSession> {
return userDao.observeUser()
}
}
// 2. ViewModel 层:将 Repository 的 Flow 转为绑定生命周期的 StateFlow
@HiltViewModel
class ProfileViewModel @Inject constructor(
userRepository: UserRepository
) : ViewModel() {
val uiState: StateFlow<ProfileUiState> = userRepository.observeUserSession()
.map { session -> ProfileUiState.Success(session) }
.stateIn(
scope = viewModelScope, // 严格收敛在 ViewModel 生命周期内
started = SharingStarted.WhileSubscribed(5000), // 后台 5 秒自动关停资源
initialValue = ProfileUiState.Loading
)
}
场景 2:一次性 UI 事件(如:弹出 Toast / 页面跳转)
开倒车的做法(全局事件挂载)
滥用全局事件总线处理页面内部或 ViewModel 与 Activity 之间的局部回调。
kotlin
// ViewModel 内部触发全局广播
FlowBus.with<String>("toast").emit("保存成功")
现代架构做法:ViewModel 与 UI 的局限于 Channel / SharedFlow
一次性 UI 事件(Side Effects)必须绑定在具体的 ViewModel 生命周期的作用域内,不允许泄漏为全局事件。
kotlin
class EditViewModel : ViewModel() {
// 使用 Channel 确保事件消费且仅消费一次
private val _uiEffect = Channel<UiEffect>(Channel.BUFFERED)
val uiEffect = _uiEffect.receiveAsFlow()
fun saveContent() {
viewModelScope.launch {
// 执行保存操作...
_uiEffect.send(UiEffect.ShowToast("保存成功"))
}
}
}
// UI 显式收集,并结合 repeatOnLifecycle 保证生命周期安全
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiEffect.collect { effect ->
when (effect) {
is UiEffect.ShowToast -> showToast(effect.message)
}
}
}
}
场景 3:多模块间的解耦通信(如:Shop 模块与 Account 模块)
开倒车的做法(隐式 EventBus 盲发)
Module A 与 Module B 为了不产生显式依赖,通过发送盲盒般的字符串/基类 Event 通信。
kotlin
// 模块 A:不知道谁会接收,直接抛出字符串事件
FlowBus.with<Map<String, String>>("module_comm").emit(mapOf("action" to "refresh"))
现代架构做法:依赖倒置原则(DIP)+ Flow 接口
在公共 Common 模块中定义响应式接口,具体业务模块提供实现,通过依赖注入(如 Hilt)进行解耦。
kotlin
// 1. 在 :common 模块中定义显式接口
interface AccountObserver {
val isPremiumUser: Flow<Boolean>
}
// 2. 在 :account 模块中实现接口并暴露依赖
class AccountObserverImpl @Inject constructor(
private val repository: UserRepository
) : AccountObserver {
override val isPremiumUser: Flow<Boolean> = repository.observeUserSession().map { it.isVIP }
}
// 3. 在 :shop 模块中直接注入接口使用,依赖关系显式且清晰
@HiltViewModel
class ShopViewModel @Inject constructor(
private val accountObserver: AccountObserver
) : ViewModel() { ... }
三、 架构对比全景表
| 维度 | 传统 EventBus / FlowBus | 现代架构范式 (SSOT + Flow + UDF) |
|---|---|---|
| 设计思想 | 事件驱动(Event-Driven) | 状态驱动(State-Driven) |
| 数据流向 | 网状/双向(隐式,难追踪) | 单向状态流 (UDF)(显式,高可预测) |
| 数据源权威 | 无权威数据源,数据分散在各事件类 | 单一数据源(Single Source of Truth) |
| 数据层流类型 | 全局强行常驻内存/热流推送 | Repository 暴露冷流 (Flow),按需拉取资源 |
| 代码可读性 | 依靠全局搜索,无法通过 IDE 直接溯源 | 显式依赖关系,可以直接 Find Usages |
| 生命周期 | 需要考虑 FlowBus 的取消订阅/粘性事件陷阱 | 绑定 viewModelScope 及 Lifecycle 自动管理 |
| 背压处理 | 容易发生缓冲溢出或挂起死锁 | 依靠 Coroutine 作用域隔离与操作符灵活控制 |
结论
现代 Android 架构淘汰的从来不是"通信需求",而是"全局隐式事件总线"这种架构设计模式。
如果仅仅是把 EventBus.getDefault().post() 换成了 FlowBus.post(),那不过是用新 API 重写了旧模式,本质上依然是在"开历史的倒车"。
真正优秀的现代架构,应该全面拥抱 SSOT(单一数据源)+ Repository(Flow) + ViewModel(StateFlow) + 依赖倒置。让数据流动在阳光下,让状态变更皆可追溯,彻底收回这个"特权",彻底告别全局事件总线。