EventBus 解决了当年的问题,但它带来的问题比解决的更多。本文用真实场景告诉你,现代 Android 架构已经不需要它了。
前言
EventBus、Otto,这些曾经风靡 Android 社区的事件总线框架确实在某个时代解决了组件间通信的难题。但随着 Kotlin 协程、Flow、ViewModel 的成熟,事件总线的种种弊端越来越难以忽视:
事件总线的问题:
- 🐛 隐式依赖:订阅/发布关系散落各处,调用链根本无法追踪
- 💥 内存泄漏:忘记
unregister是家常便饭 - 🤯 生命周期不感知:在错误的时机收到事件,轻则界面异常,重则直接崩溃
- 😵 无类型安全:早期版本依赖反射,IDE 无法静态检查,重构是噩梦
本地广播也该退场:
很多项目不用 EventBus,转而用 LocalBroadcastManager 做应用内通信,这同样是错误的:
LocalBroadcastManager已在 AndroidX 1.1.0 被官方废弃- Intent 靠字符串 key 传数据,改个字段名编译不报错,运行直接 NPE
- 广播本为跨进程/跨应用设计,用于应用内是职责错位
- 无法与 ViewModel、生命周期自然集成
本文通过真实业务场景 + 完整代码,覆盖所有事件总线的使用场景,逐一给出现代替代方案。
一、先看看我们要解决哪些场景
| 场景 | 旧方案 |
|---|---|
| Fragment 通知 Activity 更新 UI | EventBus.post(UpdateEvent()) |
| 列表页通知详情页数据变化 | EventBus.post(DataChangedEvent()) |
| 登录成功后刷新多个页面 | LocalBroadcastManager.sendBroadcast(intent) |
| Service 通知 UI 层进度更新 | LocalBroadcastManager.sendBroadcast(intent) |
| 一次性操作(Toast、导航跳转) | EventBus.post(NavEvent()) |
接下来逐一替换。
二、UI 状态更新:StateFlow + ViewModel
❌ 旧方式
✅ 新方式
无需手动注册/注销,状态是单一数据源,UI 永远与状态同步。
三、Fragment 间通信:共享 ViewModel
同一 Activity 下多个 Fragment 互相通知,是 EventBus 最高频的使用场景之一。
❌ 旧方式
✅ 新方式
两个 Fragment 通过同一个 SharedViewModel 实例通信,依赖关系显式、可测试、无副作用。
四、一次性事件(Toast、导航):SharedFlow / Channel
StateFlow 新订阅者会收到最新值,这对持续状态很合适,但对一次性事件(弹 Toast、导航跳转)则不行——页面旋转重建后会重复触发。
✅ 方案一:SharedFlow
五、全局状态与跨模块事件:Repository + Flow
这是最容易被滥用的场景,也是事件总线和本地广播的重灾区。
5.1 接口设计:Flow + suspend,而不是普通函数
很多项目把跨模块的能力抽成接口,但方法设计成了普通函数:
这和 EventBus 传状态犯的是同一个错误——拿到的是瞬时值,而不是响应式数据流,调用方只能反复主动查询,完全丢失了响应式的优势。
根据语义正确设计接口:
调用方写法更自然,真正响应式:
5.2 全局状态同步:Repository 作为单一数据源
登录成功后所有页面自动响应,退出登录同理。零广播,零事件总线。
5.3 跨模块一次性事件:SharedFlow
持续状态用 StateFlow,命令型动作信号用 SharedFlow:
5.4 Service 与 UI 通信:也不需要广播
六、完整替代方案对照表
| 场景 | ❌ 旧方案 | ✅ 现代替代方案 |
|---|---|---|
| UI 状态更新 | EventBus.post(StateEvent) | StateFlow + collect |
| Fragment 间通信 | EventBus.post / @Subscribe | 共享 ViewModel(activityViewModels()) |
| 一次性事件(Toast/导航) | EventBus.post(NavEvent) | SharedFlow 或 Channel |
| 全局登录/用户状态 | LocalBroadcastManager.sendBroadcast | Repository + StateFlow |
| 跨模块命令型事件 | LocalBroadcastManager.sendBroadcast | Repository + SharedFlow |
| Service 通知 UI | LocalBroadcastManager.sendBroadcast | @Inject Repository + StateFlow |
| 跨模块能力调用 | 直接依赖实现类 | 接口(Flow + suspend)+ Hilt 注入 |
| 跨进程/跨应用 | — | 系统 BroadcastReceiver(广播的正确用途) |
七、迁移建议
7.1 渐进式迁移策略:从“物理拆除”到“逻辑重构”
第一步:禁止新代码引入 EventBus 和 LocalBroadcastManager,在代码规范或 lint 规则中明确禁止。
第二步:找出所有存量使用点
grep -r "EventBus.getDefault()" --include="*.kt" .
grep -r "LocalBroadcastManager" --include="*.kt" .
第三步:按场景逐一替换
- 全局/跨模块状态 → 建立对应 Repository,迁移到
StateFlow - 一次性事件 → 迁移到
SharedFlow或Channel - Service 通信 → 改用
@AndroidEntryPoint+@Inject Repository
第四步:全部替换后,移除 EventBus 依赖,删除所有 @Subscribe 注解代码。
7.2 架构陷阱告警:不要用 Flow/LiveData 强行“手造总线”
这是迁移过程中最容易犯的错误:创建一个全局单例,里面塞满 MutableSharedFlow 来模拟 EventBus。
警告: 如果你发现自己在写 GlobalEventBus.post(MyEvent()),哪怕底层用的是 Kotlin Flow,你依然在制造隐式耦合和不可追踪的代码。
八、延伸:组件化场景
如果项目已经做了组件化,上述所有方案完全适用,只需遵守一个额外约定:
接口、Repository、公共 Model 都定义在 module_base,业务模块之间不允许互相依赖。
✅ module_order → module_base(依赖 UserService 接口)
✅ module_user → module_base(实现 UserService 接口)
❌ module_order → module_user(禁止!强耦合)
三板斧完全不变,只是把接口和 Repository 的定义位置从「项目内任意位置」收敛到 module_base 而已。
总结
现代 Android 架构已经完全具备替代事件总线的能力,并且在类型安全、生命周期感知、可测试性上全面胜出:
- 🔄 持续状态 →
StateFlow - 📢 一次性广播事件 →
SharedFlow - 📨 点对点事件 →
Channel - 🌐 全局/跨模块状态 →
@Singleton Repository+Flow(接口方法返回Flow或suspend) - 🔗 同级 Fragment 通信 → 共享
ViewModel - ⚙️ Service ↔ UI →
@Inject Repository+StateFlow - 📡 真正的跨进程/跨应用 → 系统
BroadcastReceiver