Flutter - 一个纯Dart的全局事件总线[global_event_bus]

24 阅读5分钟

global_event_bus 插件使用指南

如果你的 Flutter 项目已经长成了一片「林子」,页面和模块之间交流靠吼,状态同步靠心电感应——那么,是时候请出全场通讯担当:global_event_bus。它就像公司里的「大喇叭广播站」,你一嗓子喊出去,订阅了的同事都能听见,还能按优先级给领导先听。

关键词:全局事件、类型安全、优先级、延迟发送、批量处理、统计监控
适合场景:跨页面/模块通信、解耦业务、全局通知、状态联动、低耦合日志/指标收集


目录

  • 为什么是 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:addauth: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:logincart:addroute: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.v2auth: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,避免高频联动卡顿。
  • 退避重试(网络波动)
    • 监听 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 就像一个机智的主持人:

  • 不插话、不偏袒,只把话筒递给该说话的人;
  • 还顺便记笔记,告诉你今天谁说了啥、说得多不多;
  • 偶尔提醒大家别在公共频道里说「身份证号」,文明发言靠大家。

用好命名、管好订阅、做好审计,事件总线就会成为你团队的「低耦合超级胶水」。吹哨不费劲,协作更优雅。现在——请把不必要的回调链轻轻收起,给事件一点舞台!


参考资料