毁掉 Android 现代架构,只需要一个自定义的“FlowBus”

1 阅读6分钟

在 Android 架构演进的历史长河中,以 EventBus、RxBus 为代表的“全局事件总线”曾是一代神器。它一举击碎了传统 Android 嵌套回调与复杂 Interface 传值的繁琐,让跨页面通信变得轻而易举。 之前写过:

Android 现代架构不需要事件总线

Android 现代架构不需要事件总线进阶篇 (如果你没有看过,可以先看看这两篇文章)

然而,随着 Kotlin Coroutines 和 Flow 的全面普及,不少团队在重构项目时,第一件事就是用 MutableSharedFlow 封装一个所谓的 FlowBus:

// 许多项目中常见的 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 广播通知)

发送方只管抛出事件,接收方收到通知后再各自去处理逻辑,缺乏统一的数据权威来源。

// 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。

// 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 之间的局部回调。

// ViewModel 内部触发全局广播
FlowBus.with<String>("toast").emit("保存成功")

现代架构做法:ViewModel 与 UI 的局限于 Channel / SharedFlow

一次性 UI 事件(Side Effects)必须绑定在具体的 ViewModel 生命周期的作用域内,不允许泄漏为全局事件。

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 通信。

// 模块 A:不知道谁会接收,直接抛出字符串事件
FlowBus.with<Map<String, String>>("module_comm").emit(mapOf("action" to "refresh"))

现代架构做法:依赖倒置原则(DIP)+ Flow 接口

在公共 Common 模块中定义响应式接口,具体业务模块提供实现,通过依赖注入(如 Hilt)进行解耦。

// 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) + 依赖倒置。让数据流动在阳光下,让状态变更皆可追溯,彻底收回这个“特权”,彻底告别全局事件总线。