global_event_bus 插件使用指南
如果你的 Flutter 项目已经长成了一片「林子」,页面和模块之间交流靠吼,状态同步靠心电感应——那么,是时候请出全场通讯担当:global_event_bus。它就像公司里的「大喇叭广播站」,你一嗓子喊出去,订阅了的同事都能听见,还能按优先级给领导先听。
关键词:全局事件、类型安全、优先级、延迟发送、批量处理、统计监控
适合场景:跨页面/模块通信、解耦业务、全局通知、状态联动、低耦合日志/指标收集
- 插件地址:pub.dev/packages/gl…
目录
- 为什么是 global_event_bus
- 安装与初始化
- 我来喊一嗓子:发送事件
- 我来认真听:监听事件
- 事件建模(优雅又稳)
- 批量/统计/监控(让你知道「谁在吵」)
- 进阶玩法(加点「猛料」)
- 常见工作流模板
- 最佳实践(踩过的坑总结)
- 常见问题排查
- 简单 Demo(放进你的 StatefulWidget)
- 与 BLoC/Cubit 深度结合(强强联合)
- 生命周期与资源管理(避免「跑飞」)
- 测试与分层建议
- 常见坑位与规避
- 事件字典与命名规范(让事件更「有礼貌」)
- 生产级分层与目录建议(面向维护)
- 企业级事件命名规范清单(可打印版)
- 事件 Schema 契约(强类型 + JSON 示例)
- 监控与审计适配器(可插拔)
- 可视化调试面板(谁在吵一目了然)
- 隐私与安全合规(别把秘密喊出去)
- i18n 与多语言事件策略
- 模式 Cookbook(拿来就用)
- Cheat Sheet(一页速查)
- 结语(加强版)
- 参考资料
为什么是 global_event_bus
- 解耦通信:不再手拉手传参;用事件跨越组件边界,减少互相依赖。
- 类型安全:泛型事件,编译期就能纠错,不靠「runtime 祈祷」。
- 高性能:基于 Dart Stream,支持批量处理和延迟发送。
- 管理专业:事件优先级、监听一次性/多类型、全局统计与日志配置。
安装与初始化
- 添加依赖(两选一)
flutter pub add global_event_bus
或在 pubspec.yaml 中添加:
dependencies:
global_event_bus: any
- 初始化与日志配置(在
main.dart)
import 'package:flutter/material.dart';
import 'package:global_event_bus/global_event_bus.dart';
void main() {
globalEventBus.configureLogging(
const GlobalEventLogConfig(
level: EventLogLevel.debug, // 日志等级:debug|info|warning|error|none
showEventData: true, // 是否打印事件数据
showEventId: true, // 是否打印事件 ID
showListenerInfo: true, // 是否打印监听器信息
),
);
runApp(const MyApp());
}
我来喊一嗓子:发送事件
- 发送一个带数据的事件(类型安全)
globalEventBus.sendEvent<String>(
type: 'user_message',
data: 'Hello, World!',
);
- 带优先级的事件(领导优先听)
globalEventBus.sendEvent<Map<String, dynamic>>(
type: 'user_login',
data: {
'userId': '12345',
'username': 'john_doe',
'loginTime': DateTime.now().toIso8601String(),
},
priority: EventPriority.high, // critical | high | normal | low
);
- 延迟发送(三秒后再喊)
globalEventBus.sendEventDelayed<String>(
type: 'delayed_notification',
data: '三秒后我再来提醒你一下',
delay: const Duration(seconds: 3),
);
我来认真听:监听事件
- 监听指定类型事件(记得取消订阅)
import 'dart:async';
late StreamSubscription _subscription;
@override
void initState() {
super.initState();
_subscription = globalEventBus.listen<String>(
listenerId: 'my_widget_listener',
onEvent: (event) {
// event.type 是事件类型,event.data 是 String 数据
debugPrint('收到事件: ${event.type}, 数据: ${event.data}');
setState(() {});
},
);
}
@override
void dispose() {
_subscription.cancel();
super.dispose();
}
- 一次性监听(听一次就撤)
globalEventBus.listenOnce<int>(
listenerId: 'once_listener',
onEvent: (event) => debugPrint('只听一次:${event.data}'),
);
- 多类型监听(我多才多艺)
// 分别监听多种类型
final subA = globalEventBus.listen<int>(listenerId: 'math');
final subB = globalEventBus.listen<Map<String, dynamic>>(listenerId: 'json');
final subC = globalEventBus.listen<bool>(listenerId: 'flag');
// 或自定义聚合逻辑(自行在 onEvent 内判断 event.type 与数据类型)
事件建模(优雅又稳)
- 用类型标识 + 泛型数据,避免「万物皆 Map」的混乱:
class AppEvent<T> {
final String type; // e.g. 'cart:add', 'user:logout'
final T payload;
final EventPriority priority;
const AppEvent(this.type, this.payload, {this.priority = EventPriority.normal});
}
// 发送
globalEventBus.sendEvent<AppEvent<String>>(
type: 'app:event',
data: const AppEvent<String>('cart:add', 'SKU-001'),
);
- 或者直接用已有的
sendEvent<T>API,约定好type命名风格:domain:action。
批量/统计/监控(让你知道「谁在吵」)
- 插件内置统计与日志开关(见初始化的
configureLogging) - 你可以在应用的状态监控工具里,订阅所有事件类型的基础日志,做分布统计、告警等。
进阶玩法(加点「猛料」)
-
优先级场景建议
critical:安全/风控/支付等必须先处理的事件high:重要业务通知(登录状态、权限变更)normal:普通 UI 联动、页面间通信low:日志、埋点、统计等背景工作
-
邀请 BLoC/Cubit 一起玩
- 在 Cubit/Bloc 内订阅事件,统一处理业务逻辑;或把 Cubit 的状态变化广播为事件给其他模块联动(例如多页面联动)。
- Tips:事件是事件,状态是状态;分层清晰更稳健。
-
和路由/导航结合
- 页面
pop时发送结果事件;或push前发送「准备事件」给目标页面。
- 页面
常见工作流模板
-
新项目启用事件总线
- 初始化日志与配置
- 定义事件命名规范:
domain:action - 边界模块统一只通过事件通信,避免硬引用
- 公共监听入口写在顶层 Widgets/服务层
-
灰度/回滚策略
- 新事件类型上线前,预留监听「空实现」避免崩
- 日志等级可配置,线上降噪,测试放开
最佳实践(踩过的坑总结)
- 订阅要取消:
StreamSubscription.cancel(),避免内存泄漏 - 类型要匹配:监听的泛型要与发送一致,否则收不到(或类型转错)
- 事件命名规范:统一
domain:action,见名知意 - 别把事件当 RPC:事件是广播,不保证有「返回值」与「顺序依赖」
- 延迟发送在后台:注意 App 切后台后延迟行为与生命周期一致,别指望它能叫醒睡着的界面
- Isolate 边界:事件总线基于主 Isolate 的 Stream,跨 Isolate 要自定义桥接(谨慎)
常见问题排查
-
「我发了没人听」:
- 检查
type是否一致,泛型是否一致 - 确认监听是在发送之后仍处于激活状态(生命周期与
dispose) - 简化事件数据结构,先用
String/Map验证通路
- 检查
-
「我听到了两遍」:
- 重复订阅了同一个事件(多个地方
listen);或热重载导致重复实例 - 给监听器设置明确的
listenerId并统一管理
- 重复订阅了同一个事件(多个地方
-
「日志太吵」:
- 下调
GlobalEventLogConfig.level或关闭细节(showEventData=false)
- 下调
-
「延迟不生效」:
- App 生命周期影响计时器;确保在前台或用后台任务/通知机制替代
简单 Demo(放进你的 StatefulWidget)
class MessageCenter extends StatefulWidget {
const MessageCenter({super.key});
@override
State<MessageCenter> createState() => _MessageCenterState();
}
class _MessageCenterState extends State<MessageCenter> {
late final StreamSubscription _sub;
String _lastMessage = '暂无消息';
@override
void initState() {
super.initState();
_sub = globalEventBus.listen<String>(
listenerId: 'message_center',
onEvent: (e) => setState(() => _lastMessage = '${e.type}: ${e.data}'),
);
}
@override
void dispose() {
_sub.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(_lastMessage),
ElevatedButton(
onPressed: () {
globalEventBus.sendEvent<String>(type: 'user_message', data: '你点了按钮!');
},
child: const Text('广播一条消息'),
),
ElevatedButton(
onPressed: () {
globalEventBus.sendEventDelayed<String>(
type: 'delayed_notification',
data: '晚点再提醒你一次~',
delay: const Duration(seconds: 2),
);
},
child: const Text('延迟两秒再说'),
),
],
);
}
}
与 BLoC/Cubit 深度结合(强强联合)
当全局事件总线遇上 BLoC/Cubit,就像「电台主播 + 导播台」:谁在放歌(事件),谁在切台(状态),互相配合,整场节目不跑调。
- 组合原则
- 事件负责「广播事实」,状态负责「呈现结果」;不要把事件当状态,也不要让 Cubit 直接操控别人的内部数据。
- 订阅要在构造时建立,在
close()中统一取消,避免内存泄漏。 - 事件命名统一风格:
domain:action,比如cart:add、auth:login。
模式一:Cubit 作为「事件发布者」
业务更新时,既更新自身状态,也广播一条事件,供其他模块联动。
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:global_event_bus/global_event_bus.dart';
class CartItem {
final String sku;
final int count;
const CartItem(this.sku, this.count);
}
class CartState {
final List<CartItem> items;
const CartState(this.items);
}
class CartCubit extends Cubit<CartState> {
CartCubit() : super(const CartState([]));
void addItem(CartItem item) {
final next = [...state.items, item];
emit(CartState(next));
// 广播「购物车新增」事件,供其他模块(推荐页、优惠模块)联动
globalEventBus.sendEvent<CartItem>(
type: 'cart:add',
data: item,
priority: EventPriority.normal,
);
}
}
模式二:Cubit 作为「事件观察者」
订阅来自总线的事件,转换为自身状态变化。
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:global_event_bus/global_event_bus.dart';
class NotificationCubit extends Cubit<String?> {
late final StreamSubscription _sub;
NotificationCubit() : super(null) {
_sub = globalEventBus.listen<String>(
listenerId: 'notification_cubit',
onEvent: (e) {
if (e.type == 'user_message') {
emit(e.data); // 把总线来的消息显示在 UI
}
},
);
}
@override
Future<void> close() async {
await _sub.cancel();
return super.close();
}
}
模式三:Bloc 的「事件桥」(Bus → Bloc Event)
在 Bloc 的构造或 on<Event> 之前,建立总线订阅并转成 Bloc 内的事件流。
import 'dart:async';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:global_event_bus/global_event_bus.dart';
class AddItemFromBus {
final String sku;
final int count;
const AddItemFromBus(this.sku, this.count);
}
class CartBlocState {
final int totalCount;
const CartBlocState(this.totalCount);
}
class CartBloc extends Bloc<AddItemFromBus, CartBlocState> {
late final StreamSubscription _sub;
CartBloc() : super(const CartBlocState(0)) {
// 总线事件 -> Bloc 事件
_sub = globalEventBus.listen<Map<String, dynamic>>(
listenerId: 'cart_bloc_bridge',
onEvent: (e) {
if (e.type == 'cart:add') {
add(AddItemFromBus(e.data['sku'] as String, e.data['count'] as int));
}
},
);
on<AddItemFromBus>((event, emit) {
emit(CartBlocState(state.totalCount + event.count));
});
}
@override
Future<void> close() async {
await _sub.cancel();
return super.close();
}
}
模式四:UI → Bus → Bloc/Cubit(最常见的跨层协作)
UI 层发事件,不直接依赖具体 Cubit/Bloc;响应者在自己的模块内监听总线并做处理。
ElevatedButton(
onPressed: () {
globalEventBus.sendEvent<Map<String, dynamic>>(
type: 'auth:login',
data: {'userId': '42', 'token': 'hello'},
priority: EventPriority.high,
);
},
child: const Text('登录'),
);
某个认证模块的 Cubit 在构造时监听 auth:login,更新认证状态并广播二次事件(如 auth:ready)给其他模块使用。
生命周期与资源管理(避免「跑飞」)
- 在
Cubit/Bloc的构造函数或initState建立订阅,在close()/dispose()统一cancel。 - 热重载可能导致重复订阅:为监听器设置唯一
listenerId,或在构造中加幂等控制。 - 延迟发送事件在 App 后台可能被系统挂起;涉及后台行为请用通知/后台任务替代。
测试与分层建议
- Cubit/Bloc 测试中直接调用
globalEventBus.sendEvent<T>(),断言状态变化;用fake数据类型简化联动。 - 事件是跨模块的「事实广播」,状态是具体模块的「视图投影」;保持边界清晰,利于重构与复用。
- 事件名统一规范(
domain:action),并集中维护一个「事件字典」常量,减少拼写错误。
常见坑位与规避
- 泛型不匹配:
listen<T>与sendEvent<T>的T必须一致;否则不会收到或需要额外类型转换。 - 忘记取消订阅:
StreamSubscription.cancel()必须在close()/dispose()中执行。 - 把事件当 RPC:事件不保证顺序与返回值,涉及依赖顺序的业务,用专用协调器或显式调用。
- 过度依赖 Map:能用类型就用类型,能用
class就别全是Map<String, dynamic>,长期维护更省心。 - UI 线程重活:监听器里避免做重 CPU 任务,必要时把数据交给后续异步处理。
事件字典与命名规范(让事件更「有礼貌」)
- 命名风格建议:
domain:action,如auth:login、cart:add、route:pop - 统一集中管理,避免魔法字符串散落四处
/// 事件字典集中管理
abstract class AppEvents {
// 认证相关
static const String authLogin = 'auth:login';
static const String authLogout = 'auth:logout';
static const String authReady = 'auth:ready';
// 购物车
static const String cartAdd = 'cart:add';
static const String cartRemove = 'cart:remove';
// 路由与导航
static const String routePush = 'route:push';
static const String routePop = 'route:pop';
// 消息与通知
static const String userMessage = 'user_message';
static const String delayedNotification = 'delayed_notification';
}
/// 如果你想要更强类型,也可以包一层轻量事件类
class BusEvent<T> {
final String type;
final T data;
final EventPriority priority;
const BusEvent(this.type, this.data, {this.priority = EventPriority.normal});
}
// 发送时约定统一入口
void sendBus<T>(BusEvent<T> e) {
globalEventBus.sendEvent<T>(type: e.type, data: e.data, priority: e.priority);
}
- 小贴士
- 把事件字典放在
lib/core/events.dart,全局导入,杜绝拼写错误 - 对关键路径事件(登录、结算)建议配套监控与告警
- 把事件字典放在
生产级分层与目录建议(面向维护)
- 目标:事件负责跨模块协作,业务逻辑在各自模块内部收敛
- 推荐目录(示例)
lib/
core/
events.dart # 事件字典与命名集中管理
event_bus_adapters/ # 总线适配器(监控、审计等)
sentry_adapter.dart
firebase_adapter.dart
diagnostics/
bus_monitor.dart # 调试与可视化监控面板
features/
cart/
cubit/
widgets/
listeners/ # 该模块对总线的订阅集中于此
auth/
cubit/
widgets/
listeners/
xxx/
...
app.dart
订阅管理约定(防止「野生订阅」)
- 每个功能模块建立
listeners/目录,集中放置总线订阅,统一listenerId命名(如cart.listeners.recommendation)。 - 在
init时注册、在dispose/close时取消;避免随处一把梭的散落订阅。 - 跨模块协作只走总线,不直接依赖对方内部类型(通过字典约定、轻量 DTO 进行通信)。
企业级事件命名规范清单(可打印版)
- 名称结构:
domain:action[.qualifier][.vN](如pay:checkout.v2、auth:token.refresh) - 动词统一:
add|remove|update|refresh|open|close|start|stop|success|failure - 领域前缀:
auth|user|cart|order|pay|route|config|network|analytics - 小写并使用
:、.作为分隔符,不使用空格或奇怪符号 - 版本化:存在不兼容变更时在尾部加
.vN,老版本保留一段时间 - 废弃策略:先标记
deprecated,保留兼容处理与告警,再分阶段下线 - 关键事件:登录、支付、结算、权限变更需配套告警与报表
- 文档化:事件字典有描述、字段约定、示例与负责人
- 可测试性:为关键事件提供最小可复现的集成测试样例
- 审计需求:可选
traceId/actor等字段,满足追踪(见下节 Schema)
事件 Schema 契约(强类型 + JSON 示例)
- 建议在类型系统内定义轻量 DTO,同时提供 JSON 兼容结构,便于跨边界处理与日志持久化。
class AuditContext {
final String traceId; // 请求/会话追踪 ID
final String actor; // 谁触发的(用户/服务)
final String origin; // 来源(页面/模块/Isolate)
const AuditContext({required this.traceId, required this.actor, required this.origin});
}
class CheckoutEvent {
final String orderId;
final double amount;
final AuditContext ctx;
const CheckoutEvent(this.orderId, this.amount, this.ctx);
}
// 发送
globalEventBus.sendEvent<CheckoutEvent>(
type: 'pay:checkout.v2',
data: CheckoutEvent('ORD-001', 99.0, const AuditContext(traceId: 't-abc', actor: 'u-42', origin: 'cart_page')),
priority: EventPriority.high,
);
- JSON 记录示例(供日志/审计持久化):
{
"type": "pay:checkout.v2",
"ts": "2025-11-03T10:00:00Z",
"payload": {
"orderId": "ORD-001",
"amount": 99.0
},
"ctx": {
"traceId": "t-abc",
"actor": "u-42",
"origin": "cart_page"
},
"priority": "high"
}
监控与审计适配器(可插拔)
- 思路:在总线发送/接收处挂钩,路由到 Sentry/Firebase/自研埋点管线。
- 示例(伪代码):
typedef EventHook = void Function<T>(String type, T data, EventPriority p);
class SentryAdapter {
void onSend<T>(String type, T data, EventPriority p) {
// 过滤隐私 + 降噪 + 发送到监控
// Sentry.captureMessage('$type', params: {...})
}
}
final sentry = SentryAdapter();
void sendWithHooks<T>({required String type, required T data, EventPriority priority = EventPriority.normal}) {
sentry.onSend<T>(type, data, priority);
globalEventBus.sendEvent<T>(type: type, data: data, priority: priority);
}
- 低风险实践:
- 仅在生产打开关键事件的审计;开发环境打开详细日志。
- 建议对
payload做脱敏(如隐藏邮箱、手机号中间段)。
可视化调试面板(「谁在吵」一目了然)
- 在
diagnostics/bus_monitor.dart内提供一个面板,使用StreamBuilder订阅所有事件类型,显示最近 N 条事件。 - 支持搜索、过滤、导出 JSON;开发环境悬浮入口(如长按角落 3 秒)。
class BusMonitor extends StatelessWidget {
const BusMonitor({super.key});
@override
Widget build(BuildContext context) {
// 这里假设 globalEventBus 有一个调试总流(可自建聚合器)
final stream = globalEventBus.listen<String>(listenerId: 'monitor');
return Scaffold(
appBar: AppBar(title: const Text('Bus Monitor')),
body: const Center(child: Text('实现细节略,可按需订制')),
);
}
}
隐私与安全合规(别把秘密喊出去)
- 勿在事件中传递敏感信息(例如完整 Token、明文密码、完整身份证号)。
- 日志脱敏:对 PII 做掩码;对大字段做哈希;必要时仅保留引用 ID。
- 分级日志:关键事件仅记录结构化元信息;详细数据放在安全受控的管线。
- 数据最小化原则:事件只包含协作所需的最小字段。
i18n 与多语言事件策略
- 事件中携带键(key)而非本地化字符串,UI 层根据
Locale渲染文案。 - 示例:
globalEventBus.sendEvent<Map<String, dynamic>>(
type: 'toast:show',
data: {
'key': 'cart.add.success',
'args': {'count': 2},
},
);
模式 Cookbook(拿来就用)
- 防抖(用户狂点按钮)
- 用外部计时器或状态标记,在短时间内合并多次
click事件为一次处理。
- 用外部计时器或状态标记,在短时间内合并多次
- 限流(列表滚动触发过多)
- 每 300ms 处理一次
scroll:progress,避免高频联动卡顿。
- 每 300ms 处理一次
- 退避重试(网络波动)
- 监听
network:failure,触发指数退避重试并广播network:retry。
- 监听
Cheat Sheet(一页速查)
- 发送事件:
globalEventBus.sendEvent<T>(type, data, priority?) - 延迟发送:
globalEventBus.sendEventDelayed<T>(type, data, delay) - 监听事件:
globalEventBus.listen<T>(listenerId, onEvent) - 一次性监听:
globalEventBus.listenOnce<T>(listenerId, onEvent) - 管理订阅:
StreamSubscription.cancel()(可选:pause/resume) - 日志配置:
globalEventBus.configureLogging(GlobalEventLogConfig(...))
结语(加强版)
当你的应用里各种模块互相「暗号」满天飞,global_event_bus 就像一个机智的主持人:
- 不插话、不偏袒,只把话筒递给该说话的人;
- 还顺便记笔记,告诉你今天谁说了啥、说得多不多;
- 偶尔提醒大家别在公共频道里说「身份证号」,文明发言靠大家。
用好命名、管好订阅、做好审计,事件总线就会成为你团队的「低耦合超级胶水」。吹哨不费劲,协作更优雅。现在——请把不必要的回调链轻轻收起,给事件一点舞台!
参考资料
-
pub.dev:global_event_bus 插件主页
pub.dev/packages/gl… -
event_bus(对比参考:经典事件总线)
pub.dev/packages/ev… -
flutter_event_bus(对比参考:Flutter 定制事件总线)
github.com/timnew/flut…