EventBus用法以及源码解读
2026/9/7 22:10:05 网站建设 项目流程

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.1

3.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.整体思路

  1. 内部持有一个广播流 StreamController(允许多个订阅者)
  2. fire(event):发送事件
  3. on<T>():按类型过滤监听事件
  4. 支持自定义 Controller(可接入 RxDart)
  5. 提供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>(); } }

核心原理:

  1. where((e) => e is T)做类型过滤
  2. .cast<T>()类型转换,外部拿到强类型 Stream
  3. 返回的依然是 broadcast 广播流,允许多处监听
/// 发送事件 void fire(event) { streamController.add(event); } /// 关闭流,释放资源(销毁 EventBus) void destroy() { _streamController.close(); } }

3.EventBus优缺点分析

1.优点

EventBus优点如下:

  1. 代码极少,零第三方依赖
  2. 基于 Dart 原生 Stream,性能好
  3. 按类型分发,使用简洁
  4. 支持 RxDart 扩展能力

2.缺点

  1. 必须手动 cancel 订阅,页面销毁不取消会内存泄漏
  2. 全局单例模式下,整个 App 共享一条事件总线
  3. StreamController.close()之后不能再次 add 事件,destroy()一般只用于单元测试
  4. 不支持粘性事件(发送完事件之后再监听,收不到历史消息)
  5. 事件是普通对象,不支持携带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在这里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询