App中,我们经常会需要实现广播机制,用以跨页面事件通知。事件总线通常实现了订阅模式,订阅者模式包含了发布者和订阅者两种角色,可以通过事件总线来触发事件和监听事件, 下面我们实现一个简单的全局事件总线,使用单例模式。
核心原理就:单例 + Map<事件Key,订阅者列表> + 列表遍历,一个简单的实现代码示例如下:
//订阅者回调签名
typedef void EventCallback(arg);
class EventBus {
//私有构造函数
EventBus._internal();
//保存单例
static EventBus _singleton = EventBus._internal();
//工厂构造函数
factory EventBus()=> _singleton;
//保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
final _emap = Map<Object, List<EventCallback>?>();
//添加订阅者
void on(eventName, EventCallback f) {
_emap[eventName] = <EventCallback>[];
_emap[eventName]!.add(f);
}
//移除订阅者
void off(eventName, [EventCallbackf]) {
var list = _emap[eventName];
if (eventName == null || list == null) return;
if (f == null) {
_emap[eventName] = null;
} else {
list.remove(f);
}
}
//触发事件,事件触发后该事件所有订阅者会被调用
void emit(eventName, [arg]) {
var list = _emap[eventName];
if (list == null) return;
int len = list.length - 1;
//反向遍历,防止订阅者在回调中移除自身带来的下标错位
for (var i = len; i > -1; --i) {
list[i](arg);
}
}
}
//定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus
var bus = EventBus();
//页面A中,监听登录事件
bus.on("login", (arg) {
// do something
});
//登录页B中,登录成功后触发登录事件,页面A中订阅者会被调用
bus.emit("login", userInfo);
也可以用 dart:async 里的 Stream(流) 来实现:
import 'dart:async';
class EventBus {
// 使用多订阅流的形式,这种流可以有多个监听器监听(
final _streamController = StreamController.broadcast();
// 定义一个单例
static final EventBus _instance = EventBus._internal();
factory EventBus() {
return _instance;
}
EventBus._internal();
// 发布事件
void fire(event) {
_streamController.add(event);
}
// 订阅事件
StreamSubscription on<T>(void Function(T) onData) {
return _streamController.stream.where((event) => event is T).listen(onData);
}
}
// 调用处:
var eventBus = EventBus();
// 发布一个事件
eventBus.fire(UserLoggedInEvent('Alice'));
// 在其他地方订阅这个事件:
StreamSubscription subscription = eventBus.on<UserLoggedInEvent>((event) {
print('User logged in: ${event.username}');
});
//在合适的地方取消订阅
subscription.cancel();
总体来说,封装方式1是一种基于回调函数的自定义事件机制,而封装方式2是基于 Dart 内置的 Stream 和 StreamController 的事件机制。方式1相对简单,适用于基本的事件通知需求,而方式2更灵活、功能更强大,适用于复杂的场景,尤其是需要异步事件处理的情况。
选择建议:
如果项目简单,不需要支持异步事件,且对事件系统的需求较为基础,封装方式1是一个简单有效的选择。
如果项目需要支持异步事件、有复杂的事件过滤和管理需求,或者需要广播模式支持多个监听器,封装方式2是更为合适的选择。