ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EventBus用法以及源码解读

EventBus用法以及源码解读 1. 引言FlutteEventBus 是一个简单的事件总线通过使用Dart的Streams实现Application之间的解耦。2. EventBus设计模式EventBus遵循发布/订阅模式。它允许监听器订阅事件发布器触发事件​​。这使得对象之间可以进行交互而无需显式定义监听器并跟踪它们。EventBus的设计模式使得它对于解耦MVC或者MVP设计模式的应用非常有帮助。对于单个MVC各个模块之间的通信很简单如下图所示。图1.MVC设计模式但是一旦存在多个MVC组这些组就必须相互通信。这会在控制器之间造成紧密耦合。图2.多个MVC同时存在通过eventbus总线进行通信可以降低耦合度。图3.eventbus解耦多个MVC3. 基本用法使用 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.onUserLoggedInEvent().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 StreamsEventBus使用 Dart Streams 作为底层机制来跟踪监听器。您可以使用Stream API 提供的所有功能。例如您可以使用 StreamSubscriptions 来取消订阅事件。StreamSubscription loginSubscription eventBus.onUserLoggedInEvent().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)发送事件onT()按类型过滤监听事件支持自定义 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()调用时立刻执行所有监听回调执行在当前调用栈。⚠️ synctrue 容易造成嵌套调用异常事件内部再次发送事件业务一般保持默认 false。/// 监听类型为 T 的事件 StreamT onT() { // 没指定泛型 T dynamic返回全量事件流 if (T dynamic) { return streamController.stream as StreamT; } else { // 过滤只保留 event is T再强转为 StreamT return streamController.stream.where((event) event is T).castT(); } }核心原理where((e) e is T)做类型过滤.castT()类型转换外部拿到强类型 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.onX()相当于整个项目都 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); } /// 订阅指定类型事件 StreamT onT() _inner.onT(); /// 注册订阅并绑定到 [cancellables]随宿主生命周期统一取消 StreamSubscriptionT listenT( void Function(T event) handler, { ListStreamSubscriptiondynamic? cancellables, }) { final sub onT().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 BusAwareStateT extends StatefulWidget on StateT { final ListStreamSubscriptiondynamic _subscriptions []; /// 订阅并登记dispose 时统一注销 StreamSubscriptionE busListenE( 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 ValueNotifierint total ValueNotifierint(0); final ValueNotifierString? last ValueNotifierString?(null); final ListStreamSubscriptiondynamic _subscriptions []; bool _started false; void start() { if (_started) return; _started true; _subscriptions.addAll([ AppBus.instance.listenCartChangedEvent( (e) _log(购物车 ${_cartActionText(e.action)}共 ${e.items.length} 行), ), AppBus.instance.listenAuthChangedEvent( (e) _log(e.loggedIn ? 用户登录${e.nickname} : 用户登出), ), AppBus.instance.listenOrderPlacedEvent( (e) _log(订单 #${e.orderNo} 支付成功金额 ${e.totalAmount.toStringAsFixed(2)}), ), AppBus.instance.listenSessionExpiredEvent( (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(); // ① 购物车角标只关心总件数。 busListenCartChangedEvent((event) { if (!mounted) return; setState(() { _cartCount event.items.foldint(0, (sum, item) sum item.quantity); }); }); // ② AppBar 昵称登录态变化时同步。 busListenAuthChangedEvent((event) { if (!mounted) return; setState(() _nickname event.nickname); }); // ③ 下单成功 → 全局提示购物车 Tab 会自己做“最近订单”展示。 busListenOrderPlacedEvent((event) { if (!mounted) return; _toast(订单 #${event.orderNo} 支付成功金额 ${moneyText(event.totalAmount)}); }); // ④ 会话过期 → 全局提示我的 Tab 负责自动登出、埋点负责记录。 busListenSessionExpiredEvent((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在这里。
返回列表