在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(全局)变量,页面引入该文件后可以直接使用bus
var 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 {
@override
Widget 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 {
@override
Widget 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 {
@override
Widget 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);
@override
bool updateShouldNotify(_) => true;
static CountBLoC of(BuildContext context) =>
(context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider).bLoC;
}
⚠️ 注意: 这里updateShouldNotify需要传入一个InheritedWidget oldWidget,但是我们强制返回true,所以传一个“_”占位。
bloc + scoped + rxdart.zip