1. 引言
FlutteEventBus 是一个简单的事件总线,通过使用Dart的Streams实现Application之间的解耦。
2. EventBus设计模式
EventBus遵循发布/订阅模式。它允许监听器订阅事件,发布器触发事件。这使得对象之间可以进行交互,而无需显式定义监听器并跟踪它们。
EventBus的设计模式使得它对于解耦MVC或者MVP设计模式的应用非常有帮助。
对于单个MVC,各个模块之间的通信很简单,如下图所示。
图1.MVC设计模式
但是,一旦存在多个MVC组,这些组就必须相互通信。这会在控制器之间造成紧密耦合。
图2.多个MVC同时存在
通过eventbus总线进行通信,可以降低耦合度。
图3.eventbus解耦多个MVC
3. 基本用法
使用 EventBus 通常分为三步:定义事件、注册订阅者、发布事件。
3.1 添加依赖
在项目的pubspec.yaml中添加依赖:
event_bus: ^2.0.13.2 创建一个EventBus
创建 EventBus 实例并使其可供其他类使用。
通常每个应用程序只有一个事件总线,但可以设置多个事件总线来对一组特定的事件进行分组。
import 'package:event_bus/event_bus.dart'; EventBus eventBus = EventBus();注意:默认构造函数会创建一个异步事件总线。要创建同步总线,必须提供可选的 `sync: true` 属性。
3.3 定义事件
任何 Dart 类都可以用作事件。
class UserLoggedInEvent { User user; UserLoggedInEvent(this.user); } class NewOrderEvent { Order order; NewOrderEvent(this.order); }3.4 注册监听者
注册特定事件的监听器。
eventBus.on<UserLoggedInEvent>().listen((event) { // All events are of type UserLoggedInEvent (or subtypes of it). print(event.user); });注册所有时间的监听者
eventBus.on().listen((event) { // Print the runtime type. Such a set up could be used for logging. print(event.runtimeType); });关于 Dart Streams
EventBus使用 Dart Streams 作为底层机制来跟踪监听器。您可以使用Stream API 提供的所有功能。例如,您可以使用 StreamSubscriptions 来取消订阅事件。
StreamSubscription loginSubscription = eventBus.on<UserLoggedInEvent>().listen((event) { print(event.user); }); loginSubscription.cancel();3.5 发送事件
调用fire方法发送事件。
User myUser = User('Mickey'); eventBus.fire(UserLoggedInEvent(myUser));3.6 使用自定义Stream Controller
除了使用默认的 StreamController,您还可以使用以下构造函数来提供您自己的 StreamController。
例如,您可以使用 RxDart Subject 作为控制器。
import 'package:rxdart/rxdart.dart'; EventBus behaviorBus = EventBus.customController(BehaviorSubject());3.7 关闭数据流
调用destroy方法关闭底层StreamController,释放资源。
4.EventBus 源码完整解析
这是一个极简版事件总线实现,基于原生StreamController.broadcast,是Flutter最经典的轻量消息通信方案,适合跨页面、跨组件解耦通信。
1.整体思路
- 内部持有一个广播流 StreamController(允许多个订阅者)
fire(event):发送事件on<T>():按类型过滤监听事件- 支持自定义 Controller(可接入 RxDart)
- 提供
destroy()关闭流释放资源
2.源码解读
import 'dart:async'; class EventBus { // 内部流控制器 StreamController _streamController; StreamController get streamController => _streamController; // 默认构造:创建广播流,sync 控制同步/异步派发 EventBus({bool sync = false}) : _streamController = StreamController.broadcast(sync: sync); // 自定义控制器构造(兼容 RxDart Subject) EventBus.customController(StreamController controller) : _streamController = controller;参数sync区别
sync = false(默认):异步派发fire()不会立刻执行监听回调,加入微任务队列,当前代码执行完再触发。sync = true:同步派发fire()调用时立刻执行所有监听回调,执行在当前调用栈。
⚠️ sync=true 容易造成嵌套调用异常(事件内部再次发送事件),业务一般保持默认 false。
/// 监听类型为 T 的事件 Stream<T> on<T>() { // 没指定泛型 T == dynamic,返回全量事件流 if (T == dynamic) { return streamController.stream as Stream<T>; } else { // 过滤:只保留 event is T,再强转为 Stream<T> return streamController.stream.where((event) => event is T).cast<T>(); } }核心原理:
where((e) => e is T)做类型过滤.cast<T>()类型转换,外部拿到强类型 Stream- 返回的依然是 broadcast 广播流,允许多处监听
/// 发送事件 void fire(event) { streamController.add(event); } /// 关闭流,释放资源(销毁 EventBus) void destroy() { _streamController.close(); } }3.EventBus优缺点分析
1.优点
EventBus优点如下:
- 代码极少,零第三方依赖
- 基于 Dart 原生 Stream,性能好
- 按类型分发,使用简洁
- 支持 RxDart 扩展能力
2.缺点
- 必须手动 cancel 订阅,页面销毁不取消会内存泄漏
- 全局单例模式下,整个 App 共享一条事件总线
StreamController.close()之后不能再次 add 事件,destroy()一般只用于单元测试- 不支持粘性事件(发送完事件之后再监听,收不到历史消息)
- 事件是普通对象,不支持携带BuildContext(禁止在事件里传context)
和其他状态管理对比
| 方案 | 特点 |
|---|---|
| 原生 EventBus | 纯事件通知,不保存状态,适合消息广播 |
| Provider / Riverpod | 状态共享,UI自动刷新 |
| Bloc | 单向数据流,复杂业务状态管理 |
| GetX EventBus | 封装好的粘性事件版本 |
5. 用法
了解了EventBus 的基本用法和底层实现,我们下面通过一个demo,模拟下真实开发中的场景。
1.业务层封装
为什么我们要在封装一层呢?
原因如下:
1.依赖隔离业务代码里到处 eventBus.on<X>(),相当于整个项目都 import 了 event_bus。将来换实现(或升级大改 API)时只需动一个文件。
2.订阅泄漏是重灾区:StreamSubscription 忘记 cancel,监听者(尤其 State/Controller)就会被总线强引用,页面 pop 了还在收事件。封装统一提供"自动清理"能力,比每个页面手写 dispose 可靠。
3.统一拦截点:发/收事件都可以在此打日志、统计、断言,出了问题好排查。
4.约束用法:对外只暴露你认可的方法(比如禁止业务层直接拿底层 controller 乱 add)。
我们封装一个全局的AppBus类:
// lib/core/bus/app_bus.dart import 'dart:async'; import 'package:event_bus/event_bus.dart'; /// 全局唯一总线实例,业务层禁止直接使用第三方对象, /// 统一走 [AppBus] 收口,便于替换实现与加日志。 class AppBus { AppBus._(); static final AppBus instance = AppBus._(); final EventBus _inner = EventBus(); /// 发送事件 void fire(Object event) { // 统一埋点:日志 / 统计 / 断言都加在这里 _inner.fire(event); } /// 订阅指定类型事件 Stream<T> on<T>() => _inner.on<T>(); /// 注册订阅并绑定到 [cancellables],随宿主生命周期统一取消 StreamSubscription<T> listen<T>( void Function(T event) handler, { List<StreamSubscription<dynamic>>? cancellables, }) { final sub = on<T>().listen(handler); cancellables?.add(sub); return sub; } /// App 退出时清理(一般不用调) void dispose() => _inner.destroy(); }同时为了防止造成内存泄漏,我们把EventBus和Widget的State关联,实现自动回收的功能。
// lib/core/bus/bus_state.dart import 'dart:async'; import 'package:flutter/widgets.dart'; import 'app_bus.dart'; /// 混入后:页面里用 busListen() 订阅,dispose 自动 cancel,杜绝泄漏。 mixin BusAwareState<T extends StatefulWidget> on State<T> { final List<StreamSubscription<dynamic>> _subscriptions = []; /// 订阅并登记,dispose 时统一注销 StreamSubscription<E> busListen<E>( void Function(E event) handler, { Object? Function()? tag, // 可选:用于调试日志 }) { final sub = AppBus.instance.listen(handler); _subscriptions.add(sub); return sub; } @override void dispose() { for (final sub in _subscriptions) { sub.cancel(); } _subscriptions.clear(); super.dispose(); } }2.模拟真实的购物场景
在我们的demo中,模拟真实购物的场景。
具体的页面如下所示:
图4.模拟真实的购物场景
在上述的场景中,我们需要同步检测购物车中上商品的变化,用户登陆状态的变化,订单是否支付成功,当前会话是否过期等事件,然后我们根据需求做埋点/审计等服务。
1.业务代码
大致的代码如下:
class AnalyticsTracker { AnalyticsTracker._(); static final AnalyticsTracker instance = AnalyticsTracker._(); final ValueNotifier<int> total = ValueNotifier<int>(0); final ValueNotifier<String?> last = ValueNotifier<String?>(null); final List<StreamSubscription<dynamic>> _subscriptions = []; bool _started = false; void start() { if (_started) return; _started = true; _subscriptions.addAll([ AppBus.instance.listen<CartChangedEvent>( (e) => _log('购物车 ${_cartActionText(e.action)}(共 ${e.items.length} 行)'), ), AppBus.instance.listen<AuthChangedEvent>( (e) => _log(e.loggedIn ? '用户登录:${e.nickname}' : '用户登出'), ), AppBus.instance.listen<OrderPlacedEvent>( (e) => _log('订单 #${e.orderNo} 支付成功,金额 ${e.totalAmount.toStringAsFixed(2)}'), ), AppBus.instance.listen<SessionExpiredEvent>( (e) => _log('会话过期:${e.reason}'), ), ]); } void dispose() { for (final subscription in _subscriptions) { subscription.cancel(); } _subscriptions.clear(); _started = false; } void _log(String text) { total.value++; last.value = text; } static String _cartActionText(String action) => switch (action) { 'add' => '加入商品', 'update' => '调整数量', 'remove' => '移除商品', _ => '已清空', }; }2.主页面逻辑
在购物的场景中,初始化的时候,我们同步检测购物车角标、登陆状态、下单状态 、会话状态等,然后同步刷新UI。
核心代码如下:
@override void initState() { super.initState(); // 场景内购物车是常驻的:再次进入时先同步一次,避免角标/列表从旧状态错位。 _cartCount = CartStore.instance.totalCount; // 服务层:非 UI 订阅,随场景页一起 start / dispose。 _analytics.start(); // ① 购物车角标:只关心总件数。 busListen<CartChangedEvent>((event) { if (!mounted) return; setState(() { _cartCount = event.items.fold<int>(0, (sum, item) => sum + item.quantity); }); }); // ② AppBar 昵称:登录态变化时同步。 busListen<AuthChangedEvent>((event) { if (!mounted) return; setState(() => _nickname = event.nickname); }); // ③ 下单成功 → 全局提示(购物车 Tab 会自己做“最近订单”展示)。 busListen<OrderPlacedEvent>((event) { if (!mounted) return; _toast('订单 #${event.orderNo} 支付成功,金额 ${moneyText(event.totalAmount)}'); }); // ④ 会话过期 → 全局提示(我的 Tab 负责自动登出、埋点负责记录)。 busListen<SessionExpiredEvent>((event) { if (!mounted) return; _toast('会话过期:${event.reason}(我的 Tab 已收到并自动处理)'); }); }3.不同业务端发送事件
上述两部设计好之后,我们在各自的业务端发送相应的事件即可。
以商品页面为例,我们点击按钮之后,我我们发送事件,然后再业务端处理业务。
void _addToCart(BuildContext context) { CartStore.instance.add(product); // 本地即时反馈(发送方自己的事);其它端的同步由事件驱动,与本页无关。 ScaffoldMessenger.of(context) ..hideCurrentSnackBar() ..showSnackBar( SnackBar( content: Text('已加入购物车:${product.name}'), duration: const Duration(milliseconds: 800), behavior: SnackBarBehavior.floating, ), ); }4.完整demo
完整的demo在这里。