在APP中,我们经常会需要一个广播机制,用以跨页面事件通知,比如一个需要登录的APP中,页面会关注用户登录或注销事件,来进行一些状态更新。这时候,一个事件总线便会非常有用,事件总线通常实现了订阅者模式,订阅者模式包含发布者和订阅者两种角色,可以通过事件总线来触发事件和监听事件,本节我们实现一个简单的全局事件总线,
EventBus
我们使用单例模式,代码如下:
//订阅者回调签名typedef void EventCallback(arg);class EventBus {//私有构造函数EventBus._internal();//保存单例static EventBus _singleton = new EventBus._internal();//工厂构造函数factory EventBus()=> _singleton;//保存事件订阅者队列,key:事件名(id),value: 对应事件的订阅者队列var _bucketmap = new Map<Object, List<EventCallback>>();//添加订阅者void on(eventName, EventCallback f) {if (eventName == null || f == null) return;_bucketmap[eventName] ??= new List<EventCallback>();_bucketmap[eventName].add(f);}//触发事件,事件触发后该事件所有订阅者会被调用void emit(eventName, [arg]) {var list = _bucketmap[eventName];if (list == null) return;int len = list.length - 1;//反向遍历,防止订阅者在回调中移除自身带来的下标错位for (var i = len; i > -1; --i) {list[i](arg);}}//移除订阅者void off(eventName, [EventCallback f]) {var list = _bucketmap[eventName];if (eventName == null || list == null) return;if (f == null) {_bucketmap[eventName] = null;} else {list.remove(f);}}}//定义一个top-level(全局)变量,页面引入该文件后可以直接使用busvar bus = new EventBus();
使用示例:
//页面A中...//监听登录事件bus.on("login", (arg) {// do something});//登录页B中...//登录成功后触发登录事件,页面A中订阅者会被调用bus.emit("login", userInfo);
注意:Dart中实现单例模式的标准做法就是使用static变量+工厂构造函数的方式,这样就可以保证new EventBus()始终返回都是同一个实例,读者应该理解并掌握这种方法。
事件总线通常用于组件之间状态共享,但关于组件之间状态共享也有一些专门的包如redux、以及前面介绍过的Provider。对于一些简单的应用,事件总线是足以满足业务需求的,如果你决定使用状态管理包的话,一定要想清楚您的APP是否真的有必要使用它,防止“化简为繁”、过度设计。
Bloc + Stream
BLoC是一种利用 reactive programming(反应式编程)方式构建应用的方法,这是一个由流构成的完全异步的世界。
无需导包,Flutter自带‘dart:async’库
- 用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
import 'dart:async';class CountBLoC {int _count = 0;int get value => _count;// StreamController<int> _countController = StreamController<int>();var _countController = StreamController<int>.broadcast();/// 对内提供入口StreamSink get _countSink => _countController.sink;/// 提供 stream StreamBuilder 订阅Stream<int> get stream => _countController.stream;increment() {_countController.sink.add(++_count);}decrease() {_countSink.add(--_count);}dispose() {_countController.close();}}CountBLoC bLoC = CountBLoC();
2、在页面中使用StreamBuilder
main.dart
import 'package:flutter/material.dart';import 'count_bloc.dart';void main() => runApp(MyApp());/** single_global_instance 入口*/class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'single_global_instance',theme: ThemeData.dark(),home: FirstPage(),);}}/** scoped 入口*///class MyApp extends StatelessWidget {// @override// Widget build(BuildContext context) {// return BlocProvider(// child: MaterialApp(// title: 'scoped',// theme: ThemeData.dark(),// home: FirstPage(),// ),// );// }//}/** rxdart 入口*/// class MyApp extends StatelessWidget {// @override// Widget build(BuildContext context) {// return MaterialApp(// title: 'rxdart',// theme: ThemeData.dark(),// home: FirstPage(),// );// }// }// -----------------------------------------------------class FirstPage extends StatelessWidget {@overrideWidget build(BuildContext context) {print("build");return Scaffold(appBar: AppBar(title: Text('First Page'),),body: Center(child: StreamBuilder<int>(stream: bLoC.stream,initialData: bLoC.value,builder: (BuildContext context, AsyncSnapshot<int> snapshot) {return Text('You hit me: ${snapshot.data} times',);}),),floatingActionButton: FloatingActionButton(backgroundColor:Colors.white,child: Icon(Icons.arrow_forward,color: Colors.black,),onPressed: () =>Navigator.of(context).push(MaterialPageRoute(builder: (context) => SecondPage()))),);}}// -----------------------------------------------------class SecondPage extends StatelessWidget {@overrideWidget build(BuildContext context) {return Scaffold(appBar: AppBar(title: Text('Second Page'),),body: Center(child: StreamBuilder(stream: bLoC.stream,initialData: bLoC.value,builder: (context, snapshot) => Text("You hit me: ${snapshot.data} times",style: Theme.of(context).textTheme.bodyText1,)),),floatingActionButton: FloatingActionButton(onPressed: () => bLoC.increment(),child: Icon(Icons.add),),);}}
- 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。
class BlocProvider extends InheritedWidget {CountBLoC bLoC = CountBLoC();BlocProvider({Key key, Widget child}) : super(key: key, child: child);@overridebool updateShouldNotify(_) => true;static CountBLoC of(BuildContext context) =>(context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bLoC;}
⚠️ 注意: 这里updateShouldNotify需要传入一个InheritedWidget oldWidget,但是我们强制返回true,所以传一个“_”占位。
bloc + scoped + rxdart.zip
