在APP中,我们经常会需要一个广播机制,用以跨页面事件通知,比如一个需要登录的APP中,页面会关注用户登录或注销事件,来进行一些状态更新。这时候,一个事件总线便会非常有用,事件总线通常实现了订阅者模式,订阅者模式包含发布者和订阅者两种角色,可以通过事件总线来触发事件和监听事件,本节我们实现一个简单的全局事件总线,

EventBus

我们使用单例模式,代码如下:

  1. //订阅者回调签名
  2. typedef void EventCallback(arg);
  3. class EventBus {
  4. //私有构造函数
  5. EventBus._internal();
  6. //保存单例
  7. static EventBus _singleton = new EventBus._internal();
  8. //工厂构造函数
  9. factory EventBus()=> _singleton;
  10. //保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列
  11. var _bucketmap = new Map<Object, List<EventCallback>>();
  12. //添加订阅者
  13. void on(eventName, EventCallback f) {
  14. if (eventName == null || f == null) return;
  15. _bucketmap[eventName] ??= new List<EventCallback>();
  16. _bucketmap[eventName].add(f);
  17. }
  18. //触发事件,事件触发后该事件所有订阅者会被调用
  19. void emit(eventName, [arg]) {
  20. var list = _bucketmap[eventName];
  21. if (list == null) return;
  22. int len = list.length - 1;
  23. //反向遍历,防止订阅者在回调中移除自身带来的下标错位
  24. for (var i = len; i > -1; --i) {
  25. list[i](arg);
  26. }
  27. }
  28. //移除订阅者
  29. void off(eventName, [EventCallback f]) {
  30. var list = _bucketmap[eventName];
  31. if (eventName == null || list == null) return;
  32. if (f == null) {
  33. _bucketmap[eventName] = null;
  34. } else {
  35. list.remove(f);
  36. }
  37. }
  38. }
  39. //定义一个top-level(全局)变量,页面引入该文件后可以直接使用bus
  40. var bus = new EventBus();

使用示例:

  1. //页面A中
  2. ...
  3. //监听登录事件
  4. bus.on("login", (arg) {
  5. // do something
  6. });
  7. //登录页B中
  8. ...
  9. //登录成功后触发登录事件,页面A中订阅者会被调用
  10. bus.emit("login", userInfo);

注意:Dart中实现单例模式的标准做法就是使用static变量+工厂构造函数的方式,这样就可以保证new EventBus()始终返回都是同一个实例,读者应该理解并掌握这种方法。

事件总线通常用于组件之间状态共享,但关于组件之间状态共享也有一些专门的包如redux、以及前面介绍过的Provider。对于一些简单的应用,事件总线是足以满足业务需求的,如果你决定使用状态管理包的话,一定要想清楚您的APP是否真的有必要使用它,防止“化简为繁”、过度设计。

Bloc + Stream

BLoC是一种利用 reactive programming(反应式编程)方式构建应用的方法,这是一个由流构成的完全异步的世界。
无需导包,Flutter自带‘dart:async’库
image.png

  • 用StreamBuilder包裹有状态的部件,streambuilder将会监听一个流
  • 这个流来自于BLoC
  • 有状态小部件中的数据来自于监听的流。
  • 用户交互手势被检测到,产生了事件。例如按了一下按钮。
  • 调用bloc的功能来处理这个事件
  • 在bloc中处理完毕后将会吧最新的数据add进流的sink中
  • StreamBuilder监听到新的数据,产生一个新的snapshot,并重新调用build方法
  • Widget被重新构建

BLoC能够允许我们完美的分离业务逻辑!再也不用考虑什么时候需要刷新屏幕了,一切交给StreamBuilder和BLoC!和StatefulWidget说拜拜!!
BLoC代表业务逻辑组件(Business Logic Component),由来自Google的两位工程师 Paolo Soares和Cong Hui设计,并在2018年DartConf期间(2018年1月23日至24日)首次展示。点击观看Youtube视频

1、全局单例创建

全局单例我们只需要在bloc类的文件中创建一个bloc实例即可。不过我并不推荐这种做法,因为不需要用这个bloc的时候,我们应该释放它
count_bloc.dart

  1. import 'dart:async';
  2. class CountBLoC {
  3. int _count = 0;
  4. int get value => _count;
  5. // StreamController<int> _countController = StreamController<int>();
  6. var _countController = StreamController<int>.broadcast();
  7. /// 对内提供入口
  8. StreamSink get _countSink => _countController.sink;
  9. /// 提供 stream StreamBuilder 订阅
  10. Stream<int> get stream => _countController.stream;
  11. increment() {
  12. _countController.sink.add(++_count);
  13. }
  14. decrease() {
  15. _countSink.add(--_count);
  16. }
  17. dispose() {
  18. _countController.close();
  19. }
  20. }
  21. CountBLoC bLoC = CountBLoC();

2、在页面中使用StreamBuilder

main.dart

  1. import 'package:flutter/material.dart';
  2. import 'count_bloc.dart';
  3. void main() => runApp(MyApp());
  4. /*
  5. * single_global_instance 入口
  6. */
  7. class MyApp extends StatelessWidget {
  8. @override
  9. Widget build(BuildContext context) {
  10. return MaterialApp(
  11. title: 'single_global_instance',
  12. theme: ThemeData.dark(),
  13. home: FirstPage(),
  14. );
  15. }
  16. }
  17. /*
  18. * scoped 入口
  19. */
  20. //class MyApp extends StatelessWidget {
  21. // @override
  22. // Widget build(BuildContext context) {
  23. // return BlocProvider(
  24. // child: MaterialApp(
  25. // title: 'scoped',
  26. // theme: ThemeData.dark(),
  27. // home: FirstPage(),
  28. // ),
  29. // );
  30. // }
  31. //}
  32. /*
  33. * rxdart 入口
  34. */
  35. // class MyApp extends StatelessWidget {
  36. // @override
  37. // Widget build(BuildContext context) {
  38. // return MaterialApp(
  39. // title: 'rxdart',
  40. // theme: ThemeData.dark(),
  41. // home: FirstPage(),
  42. // );
  43. // }
  44. // }
  45. // -----------------------------------------------------
  46. class FirstPage extends StatelessWidget {
  47. @override
  48. Widget build(BuildContext context) {
  49. print("build");
  50. return Scaffold(
  51. appBar: AppBar(
  52. title: Text('First Page'),
  53. ),
  54. body: Center(
  55. child: StreamBuilder<int>(
  56. stream: bLoC.stream,
  57. initialData: bLoC.value,
  58. builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
  59. return Text(
  60. 'You hit me: ${snapshot.data} times',
  61. );
  62. }),
  63. ),
  64. floatingActionButton: FloatingActionButton(
  65. backgroundColor:Colors.white,
  66. child: Icon(Icons.arrow_forward,color: Colors.black,),
  67. onPressed: () =>
  68. Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondPage()))),
  69. );
  70. }
  71. }
  72. // -----------------------------------------------------
  73. class SecondPage extends StatelessWidget {
  74. @override
  75. Widget build(BuildContext context) {
  76. return Scaffold(
  77. appBar: AppBar(
  78. title: Text('Second Page'),
  79. ),
  80. body: Center(
  81. child: StreamBuilder(
  82. stream: bLoC.stream,
  83. initialData: bLoC.value,
  84. builder: (context, snapshot) => Text(
  85. "You hit me: ${snapshot.data} times",
  86. style: Theme.of(context).textTheme.bodyText1,
  87. )),
  88. ),
  89. floatingActionButton: FloatingActionButton(
  90. onPressed: () => bLoC.increment(),
  91. child: Icon(Icons.add),
  92. ),
  93. );
  94. }
  95. }
  • StreamBuilder中stream参数代表了这个stream builder监听的流,我们这里监听的是countBloc的value(它是一个stream)。
  • initData代表初始的值,因为当这个控件首次渲染的时候,还未与用户产生交互,也就不会有事件从流中流出。所以需要给首次渲染一个初始值。
  • builder函数接收一个位置参数BuildContext 以及一个snapshot。snapshot就是这个流输出的数据的一个快照。我们可以通过snapshot.data访问快照中的数据。也可以通过snapshot.hasError判断是否有异常,并通过snapshot.error获取这个异常。
  • StreamBuilder中的builder是一个AsyncWidgetBuilder,它能够异步构建widget,当检测到有数据从流中流出时,将会重新构建

扩展: Scoped模式

创建一个bloc provider类,这里我们需要借助InheritWidget,实现of方法并让updateShouldNotify返回true。

  1. class BlocProvider extends InheritedWidget {
  2. CountBLoC bLoC = CountBLoC();
  3. BlocProvider({Key key, Widget child}) : super(key: key, child: child);
  4. @override
  5. bool updateShouldNotify(_) => true;
  6. static CountBLoC of(BuildContext context) =>
  7. (context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bLoC;
  8. }

⚠️ 注意: 这里updateShouldNotify需要传入一个InheritedWidget oldWidget,但是我们强制返回true,所以传一个“_”占位。
bloc + scoped + rxdart.zip