很是感谢 Didier Boelens 赞成我将它的一些文章翻译为中文发表,这是其中一篇。react
本文应用多个实例详细讲解了 BLoC 设计模式的原理和使用方法,很是值得学习。git
原文 连接github
特别提醒:本文很长,阅读完须要较长时间正则表达式
BLoC 设计模式、响应式编程、流、应用案例、和有用的模式。编程
难度:中等设计模式
前一段时间,我介绍了 BLoC、响应式编程(Reactive Programming )、流(Streams) 的概念后,我想给你们分享一些我常用而且很是有用的(至少对我而言)模式应该是一件有趣的事。bash
我本文要讲的主题有这些:服务器
BLoC Provider 和 InheritedWidgetmarkdown
容许根据事件响应状态的改变
容许根据输入和验证规则控制表单的行为 个人例子还包括密码和从新输入密码的比较。
容许一个 Widget 根据其是否存在某一个列表中来调整其行为。
本文完整的代码在 GitHub 上能够获取到。
我借此文章的机会介绍我另外一个版本的 BlocProvider,它如今依赖一个 InheritedWidget。
使用 InheritedWidget 的好处是咱们能够提升 APP 的 性能。
请容我细细道来……
我以前版本的 BlocProvider 实现为一个常规 StatefulWidget,以下所示:
abstract class BlocBase {
void dispose();
}
// Generic BLoC provider
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final T bloc;
final Widget child;
@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<BlocProvider<T>>();
BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
return provider.bloc;
}
static Type _typeOf<T>() => T;
}
class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
@override
void dispose(){
widget.bloc.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return widget.child;
}
}
复制代码
我使用 StatefulWidget 利用其 dispose() 方法以确保在再也不须要时释放 BLoC 分配的资源。
这很好用,但从性能角度来看并非最佳的。
context.ancestorWidgetOfExactType() 是一个 O(n) 复杂度的方法。为了获取须要的某种特定类型的祖先,它从上下文开始向上遍历树,一次递归地向上移动一个父节点,直到完成。若是从当前上下文到祖先的距离很小,则调用此函数仍是能够接受的,不然应该避免调用此函数。 这是这个函数的代码。
@override
Widget ancestorWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element ancestor = _parent;
while (ancestor != null && ancestor.widget.runtimeType != targetType)
ancestor = ancestor._parent;
return ancestor?.widget;
}
复制代码
新的实现方式依赖于 StatefulWidget,并结合了 InheritedWidget:
Type _typeOf<T>() => T;
abstract class BlocBase {
void dispose();
}
class BlocProvider<T extends BlocBase> extends StatefulWidget {
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}): super(key: key);
final Widget child;
final T bloc;
@override
_BlocProviderState<T> createState() => _BlocProviderState<T>();
static T of<T extends BlocBase>(BuildContext context){
final type = _typeOf<_BlocProviderInherited<T>>();
_BlocProviderInherited<T> provider =
context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
return provider?.bloc;
}
}
class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<T>>{
@override
void dispose(){
widget.bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return new _BlocProviderInherited<T>(
bloc: widget.bloc,
child: widget.child,
);
}
}
class _BlocProviderInherited<T> extends InheritedWidget {
_BlocProviderInherited({
Key key,
@required Widget child,
@required this.bloc,
}) : super(key: key, child: child);
final T bloc;
@override
bool updateShouldNotify(_BlocProviderInherited oldWidget) => false;
}
复制代码
这种解决方式的优势是 性能 更高。
因为使用了 InheritedWidget,如今能够调用 context.ancestorInheritedElementForWidgetOfExactType() 方法,它是一个 O(1) 复杂度的方法,这意味着获取祖先节点是很是快的,如其源代码所示:
@override
InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) {
assert(_debugCheckStateIsActiveForAncestorLookup());
final InheritedElement ancestor = _inheritedWidgets == null
? null
: _inheritedWidgets[targetType];
return ancestor;
}
复制代码
这也代表全部 InheritedWidgets 都由 Framework 保存。
为何要使用 ancestorInheritedElementForWidgetOfExactType 呢 ?
你应该已经注意到了我用 ancestorInheritedElementForWidgetOfExactType 代替了一般使用的 inheritFromWidgetOfExactType 方法。
缘由是我不但愿调用 BlocProvider 的上下文被注册为 InheritedWidget 的依赖项,由于我不须要它。
Widget build(BuildContext context){
return BlocProvider<MyBloc>{
bloc: myBloc,
child: ...
}
}
复制代码
Widget build(BuildContext context){
MyBloc myBloc = BlocProvider.of<MyBloc>(context);
...
}
复制代码
要回答这个问题,您须要弄清楚其使用范围。
假如您必须处理一些与用户身份验证或用户简介、用户首选项、购物车相关的一些业务逻辑…… 任何须要从应用程序的任何可能地方(例如,从不一样页面)均可以 获取到 BLoC 的业务逻辑,有两种方式 可使这个 BLoC 在任何地方均可以访问。
此解决方案依赖于使用全局对象,(为全部使用的地方)实例化一次,不是任何 Widget 树 的一部分。
import 'package:rxdart/rxdart.dart';
class GlobalBloc {
///
/// Streams related to this BLoC
///
BehaviorSubject<String> _controller = BehaviorSubject<String>();
Function(String) get push => _controller.sink.add;
Stream<String> get stream => _controller;
///
/// Singleton factory
///
static final GlobalBloc _bloc = new GlobalBloc._internal();
factory GlobalBloc(){
return _bloc;
}
GlobalBloc._internal();
///
/// Resource disposal
///
void dispose(){
_controller?.close();
}
GlobalBloc globalBloc = GlobalBloc();
复制代码
要使用此 BLoC,您只需导入该类并直接调用其方法,以下所示:
import 'global_bloc.dart';
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
globalBloc.push('building MyWidget');
return Container();
}
}
复制代码
若是您须要一个 惟一 的 BLoC 并须要从应用程序内部的任何位置访问,这是一个可接受的解决方案。
许多纯粹主义者反对这种解决方案。 我不知道为何,可是…因此让咱们看看另外一个实现方式吧 ......
在 Flutter 中,全部页面的祖先自己必须是 MaterialApp 的父级 。这是由于一个页面(或路径)是被包装在 一个 OverlayEntry 中的,是全部页面 栈 的一个子项。
换句话说,每一个页面都有一个 独立于任何其余页面 的 Buildcontext。 这就解释了为何在不使用任何技巧的状况下,两个页面(或路由)不可能有任何共同的地方。
所以,若是您须要在应用程序中的任何位置使用 BLoC,则必须将其做为 MaterialApp 的父级,以下所示:
void main() => runApp(Application());
class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
bloc: AuthenticationBloc(),
child: MaterialApp(
title: 'BLoC Samples',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: InitializationPage(),
),
);
}
}
复制代码
在大多数状况下,您可能须要在应用程序的某些特定部分使用某一个 BLoC。
做为一个例子,咱们能够想象一个讨论相关的模块,它的 BLoC 将会用于:
在这个例子中,你不须要使这个 BLoC 在整个应用的任何地方均可用,只须要在一些 Widget 中可用(树的一部分)。
第一种解决方案多是将 BLoC 注入到 Widget 树 的根节点,以下所示:
class MyTree extends StatelessWidget {
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: MyBloc(),
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
MyBloc = BlocProvider.of<MyBloc>(context);
return Container();
}
}
复制代码
这样,全部 Widget 均可用经过调用 BlocProvider.of 方法访问 BLoC。
边注
如上所示的解决方案并非最佳的,由于它将在每次从新构建(rebuild)时实例化BLoC。
后果:
- 您将丢失 BLoC 的任何现有的内容
- 它会耗费 CPU 时间,由于它须要在每次构建时实例化它。
在这种状况下,更好的方法是使用 StatefulWidget 从其持久状态中受益,以下所示:
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => _MyTreeState();
}
class _MyTreeState extends State<MyTree>{
MyBloc bloc;
@override
void initState(){
super.initState();
bloc = MyBloc();
}
@override
void dispose(){
bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context){
return BlocProvider<MyBloc>(
bloc: bloc,
child: Column(
children: <Widget>[
MyChildWidget(),
],
),
);
}
}
复制代码
使用这种方法,若是须要从新构建 “MyTree” Widget ,则没必要从新实例化 BLoC 并直接重用现有实例。
这涉及到 一个 BLoC 仅由一个 Widget 使用的状况。 在这种状况下,能够在 Widget 中实例化 BLoC。
有时,处理一系列多是顺序或并行,长或短,同步或异步以及可能致使各类结果的操做可能变得很是难以编程。您可能还须要根据状态的改变或进度更新显示。
此第一个例子旨在使这种状况更容易处理。
该解决方案基于如下原则:
为了说明这个概念,咱们来看两个常见的例子:
应用初始化
假设您须要执行一系列操做来初始化一个应用程序。 这些操做可能有与服务器的交互(例如,加载一些数据)。 在此初始化过程当中,您可能须要显示进度条和一系列图像以使用户等待。
用户认证 在启动时,应用程序可能要求用户进行身份验证或注册。 用户经过身份验证后,将重定向到应用程序的主页面。 而后,若是用户注销,则将其重定向到认证页面。
为了可以处理全部可能的状况,事件序列,而且若是咱们认为能够在应用程序中的任何地方触发这些事件,这可能变得很是难以管理。
这就是 BlocEventState 与 BlocEventStateBuilder 相结合能够有很大帮助的地方……
BlocEventState 背后的思想是定义这样一个 BLoC:
下图显示了这个思想:
这是这个类的源代码。 解释在代码后面:
import 'package:blocs/bloc_helpers/bloc_provider.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
abstract class BlocEvent extends Object {}
abstract class BlocState extends Object {}
abstract class BlocEventStateBase<BlocEvent, BlocState> implements BlocBase {
PublishSubject<BlocEvent> _eventController = PublishSubject<BlocEvent>();
BehaviorSubject<BlocState> _stateController = BehaviorSubject<BlocState>();
///
/// To be invoked to emit an event
///
Function(BlocEvent) get emitEvent => _eventController.sink.add;
///
/// Current/New state
///
Stream<BlocState> get state => _stateController.stream;
///
/// External processing of the event
///
Stream<BlocState> eventHandler(BlocEvent event, BlocState currentState);
///
/// initialState
///
final BlocState initialState;
//
// Constructor
//
BlocEventStateBase({
@required this.initialState,
}){
//
// For each received event, we invoke the [eventHandler] and
// emit any resulting newState
//
_eventController.listen((BlocEvent event){
BlocState currentState = _stateController.value ?? initialState;
eventHandler(event, currentState).forEach((BlocState newState){
_stateController.sink.add(newState);
});
});
}
@override
void dispose() {
_eventController.close();
_stateController.close();
}
}
复制代码
如您所见,这是一个须要继承的抽象类,用于定义 eventHandler 方法的行为。
它暴露了:
在初始化时(请参阅构造函数):
用于实现此 BlocEventState 的泛型类在下面给出。 以后,咱们将实现一个真实的类。
class TemplateEventStateBloc extends BlocEventStateBase<BlocEvent, BlocState> {
TemplateEventStateBloc()
: super(
initialState: BlocState.notInitialized(),
);
@override
Stream<BlocState> eventHandler( BlocEvent event, BlocState currentState) async* {
yield BlocState.notInitialized();
}
}
复制代码
若是这个泛型类不能经过编译,请不要担忧……这是正常的,由于咱们尚未定义 BlocState.notInitialized() …… 这将在几分钟内出现。
此泛型类仅在初始化时提供 initialState 并重写了 eventHandler。
这里有一些很是有趣的事情须要注意。 咱们使用了异步生成器:**async *** 和 yield 语句。
使用 async* 修饰符标记函数,将函数标识为异步生成器:
每次调用 yield 语句时,它都会将 yield 后面的表达式结果添加到输出 Stream 中。
若是咱们须要经过一系列操做发出一系列状态(咱们稍后会在实践中看到),这将特别有用
有关异步生成器的其余详细信息,请点击此连接。
正如您所注意到的,咱们已经定义了一个 BlocEvent 和 BlocState 抽象类。
这些类须要你使用想要发出的特定的事件和状态去 继承。
这个模式的最后一部分是 BlocEventStateBuilder Widget,它容许您响应 BlocEventState 发出的 State。
这是它的源代码:
typedef Widget AsyncBlocEventStateBuilder<BlocState>(BuildContext context, BlocState state);
class BlocEventStateBuilder<BlocEvent,BlocState> extends StatelessWidget {
const BlocEventStateBuilder({
Key key,
@required this.builder,
@required this.bloc,
}): assert(builder != null),
assert(bloc != null),
super(key: key);
final BlocEventStateBase<BlocEvent,BlocState> bloc;
final AsyncBlocEventStateBuilder<BlocState> builder;
@override
Widget build(BuildContext context){
return StreamBuilder<BlocState>(
stream: bloc.state,
initialData: bloc.initialState,
builder: (BuildContext context, AsyncSnapshot<BlocState> snapshot){
return builder(context, snapshot.data);
},
);
}
}
复制代码
这个 Widget 只是一个专门的 StreamBuilder,它会在每次发出新的 BlocState 时调用传入的 builder 参数。
OK,如今咱们已经拥有了 EventStateBloc 设计模式 全部的部分了,如今是时候展现咱们能够用它们作些什么了......
第一个示例说明了您须要应用程序在启动时执行某些任务的状况。
一般的用途是,游戏在显示实际主屏幕以前,最初显示启动画面(动画与否),同时从服务器获取一些文件,检查新的更新是否可用,尝试链接到任何游戏中心……。为了避免给用户程序什么都没作的感受,它可能会显示一个进度条并按期显示一些图片,同时它会完成全部初始化过程。
我要展现的具体实现很是简单。 它只会在屏幕上显示一些完成百分比,但这能够根据你的需求很容易地扩展。
首先要作的是定义事件和状态......
在这个例子中,我只考虑2个事件:
这是定义:
class ApplicationInitializationEvent extends BlocEvent {
final ApplicationInitializationEventType type;
ApplicationInitializationEvent({
this.type: ApplicationInitializationEventType.start,
}) : assert(type != null);
}
enum ApplicationInitializationEventType {
start,
stop,
}
复制代码
这个类将提供与初始化过程相关的信息。
对于这个例子,我会考虑:
2个标志:
进度完成率
这是代码:
class ApplicationInitializationState extends BlocState {
ApplicationInitializationState({
@required this.isInitialized,
this.isInitializing: false,
this.progress: 0,
});
final bool isInitialized;
final bool isInitializing;
final int progress;
factory ApplicationInitializationState.notInitialized() {
return ApplicationInitializationState(
isInitialized: false,
);
}
factory ApplicationInitializationState.progressing(int progress) {
return ApplicationInitializationState(
isInitialized: progress == 100,
isInitializing: true,
progress: progress,
);
}
factory ApplicationInitializationState.initialized() {
return ApplicationInitializationState(
isInitialized: true,
progress: 100,
);
}
}
复制代码
此 BLoC 负责处理基于事件的初始化过程。
这是代码:
class ApplicationInitializationBloc extends BlocEventStateBase<ApplicationInitializationEvent, ApplicationInitializationState> {
ApplicationInitializationBloc()
: super(
initialState: ApplicationInitializationState.notInitialized(),
);
@override
Stream<ApplicationInitializationState> eventHandler(
ApplicationInitializationEvent event, ApplicationInitializationState currentState) async* {
if (!currentState.isInitialized){
yield ApplicationInitializationState.notInitialized();
}
if (event.type == ApplicationInitializationEventType.start) {
for (int progress = 0; progress < 101; progress += 10){
await Future.delayed(const Duration(milliseconds: 300));
yield ApplicationInitializationState.progressing(progress);
}
}
if (event.type == ApplicationInitializationEventType.stop){
yield ApplicationInitializationState.initialized();
}
}
}
复制代码
一些解释:
当收到 “ApplicationInitializationEventType.start” 事件时,它从0到100开始计数(步骤10),而且对于每一个值(0,10,20,……),它发出(经过yield)一个新状态,以通知 BLoC 初始化正在进行中(isInitializing = true)及其进度值。
当收到 “ApplicationInitializationEventType.stop” 事件时,它认为初始化已完成。
正如你所看到的,我在计数器循环中设置了一些延迟。 这将向您展现如何使用任何 Future(例如,您须要联系服务器的状况)
如今,剩下的部分是显示 计数器的假的启动画面 ......
class InitializationPage extends StatefulWidget {
@override
_InitializationPageState createState() => _InitializationPageState();
}
class _InitializationPageState extends State<InitializationPage> {
ApplicationInitializationBloc bloc;
@override
void initState(){
super.initState();
bloc = ApplicationInitializationBloc();
bloc.emitEvent(ApplicationInitializationEvent());
}
@override
void dispose(){
bloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext pageContext) {
return SafeArea(
child: Scaffold(
body: Container(
child: Center(
child: BlocEventStateBuilder<ApplicationInitializationEvent, ApplicationInitializationState>(
bloc: bloc,
builder: (BuildContext context, ApplicationInitializationState state){
if (state.isInitialized){
//
// 一旦初始化完成,跳转到其余页面
//
WidgetsBinding.instance.addPostFrameCallback((_){
Navigator.of(context).pushReplacementNamed('/home');
});
}
return Text('Initialization in progress... ${state.progress}%');
},
),
),
),
),
);
}
}
复制代码
说明:
因为 ApplicationInitializationBloc 不须要在应用程序的任何地方使用,咱们能够在一个 StatefulWidget 中初始化它;
咱们直接发出 ApplicationInitializationEventType.start 事件来触发 eventHandler
每次发出 ApplicationInitializationState 时,咱们都会更新文本
初始化完成后,咱们将用户重定向到主页。
技巧
因为咱们没法直接在构建器内部重定向到主页,咱们使用 WidgetsBinding.instance.addPostFrameCallback() 方法请求 Flutter 在渲染完成后当即执行方法
对于此示例,我将考虑如下用例:
在启动时,若是用户未通过身份验证,则会自动显示“身份验证/注册”页面;
在用户认证期间,显示 CircularProgressIndicator;
通过身份验证后,用户将被重定向到主页;
在应用程序的任何地方,用户均可以注销;
当用户注销时,用户将自动重定向到“身份验证”页面。
固然,颇有可能以编程方式处理全部这些,但将全部这些委托给 BLoC 要容易得多。
下图解释了我要讲解的解决方案:
名为 “DecisionPage” 的中间页面将负责将用户自动重定向到“身份验证”页面或主页,具体取决于用户身份验证的状态。 固然,此 DecisionPage 从不显示,也不该被视为页面。
首先要作的是定义事件和状态......
在这个例子中,我只考虑2个事件:
这是定义:
abstract class AuthenticationEvent extends BlocEvent {
final String name;
AuthenticationEvent({
this.name: '',
});
}
class AuthenticationEventLogin extends AuthenticationEvent {
AuthenticationEventLogin({
String name,
}) : super(
name: name,
);
}
class AuthenticationEventLogout extends AuthenticationEvent {}
复制代码
该类将提供与身份验证过程相关的信息。
对于这个例子,我将考虑:
3个标志:
通过身份验证的用户名
这是它的源代码:
class AuthenticationState extends BlocState {
AuthenticationState({
@required this.isAuthenticated,
this.isAuthenticating: false,
this.hasFailed: false,
this.name: '',
});
final bool isAuthenticated;
final bool isAuthenticating;
final bool hasFailed;
final String name;
factory AuthenticationState.notAuthenticated() {
return AuthenticationState(
isAuthenticated: false,
);
}
factory AuthenticationState.authenticated(String name) {
return AuthenticationState(
isAuthenticated: true,
name: name,
);
}
factory AuthenticationState.authenticating() {
return AuthenticationState(
isAuthenticated: false,
isAuthenticating: true,
);
}
factory AuthenticationState.failure() {
return AuthenticationState(
isAuthenticated: false,
hasFailed: true,
);
}
}
复制代码
此 BLoC 负责根据事件处理身份验证过程。
这是代码:
class AuthenticationBloc extends BlocEventStateBase<AuthenticationEvent, AuthenticationState> {
AuthenticationBloc()
: super(
initialState: AuthenticationState.notAuthenticated(),
);
@override
Stream<AuthenticationState> eventHandler(
AuthenticationEvent event, AuthenticationState currentState) async* {
if (event is AuthenticationEventLogin) {
// Inform that we are proceeding with the authentication
yield AuthenticationState.authenticating();
// Simulate a call to the authentication server
await Future.delayed(const Duration(seconds: 2));
// Inform that we have successfuly authenticated, or not
if (event.name == "failure"){
yield AuthenticationState.failure();
} else {
yield AuthenticationState.authenticated(event.name);
}
}
if (event is AuthenticationEventLogout){
yield AuthenticationState.notAuthenticated();
}
}
}
复制代码
一些解释:
正如您将要看到的那样,为了便于解释,此页面很是基本且不会作太多内容。
这是代码。 解释稍后给出:
class AuthenticationPage extends StatelessWidget {
///
/// Prevents the use of the "back" button
///
Future<bool> _onWillPopScope() async {
return false;
}
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return WillPopScope(
onWillPop: _onWillPopScope,
child: SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Authentication Page'),
leading: Container(),
),
body: Container(
child:
BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
bloc: bloc,
builder: (BuildContext context, AuthenticationState state) {
if (state.isAuthenticating) {
return PendingAction();
}
if (state.isAuthenticated){
return Container();
}
List<Widget> children = <Widget>[];
// Button to fake the authentication (success)
children.add(
ListTile(
title: RaisedButton(
child: Text('Log in (success)'),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogin(name: 'Didier'));
},
),
),
);
// Button to fake the authentication (failure)
children.add(
ListTile(
title: RaisedButton(
child: Text('Log in (failure)'),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogin(name: 'failure'));
},
),
),
);
// Display a text if the authentication failed
if (state.hasFailed){
children.add(
Text('Authentication failure!'),
);
}
return Column(
children: children,
);
},
),
),
),
),
);
}
}
复制代码
说明:
就是这样! 没有别的事情须要作了……很简单,不是吗?
提示:
您可能已经注意到,我将页面包装在 WillPopScope 中。 理由是我不但愿用户可以使用 Android '后退' 按钮,如此示例中所示,身份验证是一个必须的步骤,它阻止用户访问任何其余部分,除非通过正确的身份验证。
如前所述,我但愿应用程序根据身份验证状态自动重定向到 AuthenticationPage 或 HomePage。
如下是此 DecisionPage 的代码,说明在代码后面:
class DecisionPage extends StatefulWidget {
@override
DecisionPageState createState() {
return new DecisionPageState();
}
}
class DecisionPageState extends State<DecisionPage> {
AuthenticationState oldAuthenticationState;
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return BlocEventStateBuilder<AuthenticationEvent, AuthenticationState>(
bloc: bloc,
builder: (BuildContext context, AuthenticationState state) {
if (state != oldAuthenticationState){
oldAuthenticationState = state;
if (state.isAuthenticated){
_redirectToPage(context, HomePage());
} else if (state.isAuthenticating || state.hasFailed){
//do nothing
} else {
_redirectToPage(context, AuthenticationPage());
}
}
// This page does not need to display anything since it will
// always remind behind any active page (and thus 'hidden').
return Container();
}
);
}
void _redirectToPage(BuildContext context, Widget page){
WidgetsBinding.instance.addPostFrameCallback((_){
MaterialPageRoute newRoute = MaterialPageRoute(
builder: (BuildContext context) => page
);
Navigator.of(context).pushAndRemoveUntil(newRoute, ModalRoute.withName('/decision'));
});
}
}
复制代码
提醒
为了详细解释这一点,咱们须要回到Flutter处理Pages(= Route)的方式。 要处理路由,咱们使用 Navigator,它建立一个 Overlay 。
这个 Overlay 是一个 OverlayEntry 堆栈,每一个都包含一个 Page。
当咱们经过 Navigator.of(context) 压入,弹出,替换页面时,后者更新其 从新构建(rebuild) 的 Overlay (此堆栈)。
从新构建堆栈时,每一个 OverlayEntry(包括其内容) 也会 从新构建。
所以,当咱们经过 Navigator.of(context) 进行操做时,全部剩余的页面都会从新构建!
那么,为何我将它实现为 StatefulWidget ?
为了可以响应 AuthenticationState 的任何更改,此 “页面” 须要在应用程序的整个生命周期中保持存在。
这意味着,根据上面的提醒,每次 Navigator.of(context) 完成操做时,都会从新构建此页面。
所以,它的 BlocEventStateBuilder 也将重建,调用本身的 builder 方法。
由于此 builder 负责将用户重定向到与 AuthenticationState 对应的页面,因此若是咱们每次从新构建页面时重定向用户,它将继续重定向,由于不断地从新构建。
为了防止这种状况发生,咱们只须要记住咱们采起操做的最后一个 AuthenticationState,而且只在收到另外一个 AuthenticationState 时采起另外一个动做。
这是如何起做用的?
如上所述,每次发出AuthenticationState 时,BlocEventStateBuilder 都会调用其 builder 。
基于状态标志(isAuthenticated),咱们知道咱们须要向哪一个页面重定向用户。
技巧
因为咱们没法直接从构建器重定向到另外一个页面,所以咱们使用WidgetsBinding.instance.addPostFrameCallback() 方法在呈现完成后请求 Flutter 执行方法
此外,因为咱们须要在重定向用户以前删除任何现有页面,除了须要保留在全部状况下的此 DecisionPage 以外,咱们使用 Navigator.of(context).pushAndRemoveUntil(…) 来实现此目的。
为了让用户退出,您如今能够建立一个 “LogOutButton” 并将其放在应用程序的任何位置。
此按钮只须要发出 AuthenticationEventLogout() 事件,这将致使如下自动操做链:
这是此按钮的代码:
class LogOutButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
AuthenticationBloc bloc = BlocProvider.of<AuthenticationBloc>(context);
return IconButton(
icon: Icon(Icons.exit_to_app),
onPressed: () {
bloc.emitEvent(AuthenticationEventLogout());
},
);
}
}
复制代码
因为 AuthenticationBloc 须要在此应用程序的任何页面可用,咱们仍是将其做为MaterialApp的父级注入,以下所示:
void main() => runApp(Application());
class Application extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider<AuthenticationBloc>(
bloc: AuthenticationBloc(),
child: MaterialApp(
title: 'BLoC Samples',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: DecisionPage(),
),
);
}
}
复制代码
BLoC 的另外一个有趣应用是当您须要验证表单时:
我如今要作的一个例子是 RegistrationForm,它由3个TextFields(电子邮件,密码,确认密码)和1个RaisedButton组成,以启动注册过程。
我想要实现的业务规则是:
此 BLoC 负责处理验证业务规则,如前所述。
这是它的源代码:
class RegistrationFormBloc extends Object with EmailValidator, PasswordValidator implements BlocBase {
final BehaviorSubject<String> _emailController = BehaviorSubject<String>();
final BehaviorSubject<String> _passwordController = BehaviorSubject<String>();
final BehaviorSubject<String> _passwordConfirmController = BehaviorSubject<String>();
//
// Inputs
//
Function(String) get onEmailChanged => _emailController.sink.add;
Function(String) get onPasswordChanged => _passwordController.sink.add;
Function(String) get onRetypePasswordChanged => _passwordConfirmController.sink.add;
//
// Validators
//
Stream<String> get email => _emailController.stream.transform(validateEmail);
Stream<String> get password => _passwordController.stream.transform(validatePassword);
Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
.doOnData((String c){
// If the password is accepted (after validation of the rules)
// we need to ensure both password and retyped password match
if (0 != _passwordController.value.compareTo(c)){
// If they do not match, add an error
_passwordConfirmController.addError("No Match");
}
});
//
// Registration button
Stream<bool> get registerValid => Observable.combineLatest3(
email,
password,
confirmPassword,
(e, p, c) => true
);
@override
void dispose() {
_emailController?.close();
_passwordController?.close();
_passwordConfirmController?.close();
}
}
复制代码
让我详细解释一下......
OK,如今是时候深刻了解更多细节......
您可能已经注意到,此类的签名有点特殊。 咱们来回顾一下吧。
class RegistrationFormBloc extends Object with EmailValidator, PasswordValidator implements BlocBase {
...
}
复制代码
With 关键字表示此类正在使用 MIXINS(=“在另外一个类中重用某些类代码的方法”),而且为了可以使用 with 关键字,该类须要继承 Object 类。 这些 mixin 分别包含验证电子邮件和密码的代码。
有关 Mixins 的更多详细信息,我建议您阅读 Romain Rastel 的这篇精彩文章。
我只会解释EmailValidator,由于PasswordValidator很是类似。
首先,代码:
const String _kEmailRule = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";
class EmailValidator {
final StreamTransformer<String,String> validateEmail =
StreamTransformer<String,String>.fromHandlers(handleData: (email, sink){
final RegExp emailExp = new RegExp(_kEmailRule);
if (!emailExp.hasMatch(email) || email.isEmpty){
sink.addError('Entre a valid email');
} else {
sink.add(email);
}
});
}
复制代码
该类公开了一个 final 函数(“validateEmail”),它是一个 StreamTransformer。
提醒
StreamTransformer 的调用方式以下:stream.transform(StreamTransformer)。
StreamTransformer 从 Stream 经过 transformmethod 引用它的输入。 而后处理此输入,并将转换后的输入从新注入初始 Stream。
在此代码中,输入的处理包括根据正则表达式进行检查。 若是输入与正则表达式匹配,咱们只需将输入从新注入流中,不然,咱们会向流中注入错误消息。
如前所述,若是验证成功,StreamTransformer 会将输入从新注入 Stream。 为何这样作是有用的?
如下是与 Observable.combineLatest3() 相关的解释…此方法在它引用的全部 Streams 至少发出一个值以前不会发出任何值。
让咱们看看下面的图片来讲明咱们想要实现的目标。
若是用户输入电子邮件而且后者通过验证,它将由电子邮件流发出,它将是 Observable.combineLatest3() 的一个输入;
若是电子邮件无效,则会向流中添加错误(而且流中没有值);
这一样适用于密码和从新输入密码;
当全部这三个验证都成功时(意味着全部这三个流都会发出一个值),Observable.combineLatest3() 将由 “(e,p,c)=> true”,发出一个true(见 第35行)。
我在互联网上看到了不少与这种比较有关的问题。 存在几种解决方案,让我解释其中的两种。
第一个解决方案多是下面这样的:
Stream<bool> get registerValid => Observable.combineLatest3(
email,
password,
confirmPassword,
(e, p, c) => (0 == p.compareTo(c))
);
复制代码
这种解决方案简单地比较这两个密码,当它们验证经过且相互匹配,发出一个值(= true)。
咱们很快就会看到,Register 按钮的可访问性将取决于registerValid 流。
若是两个密码不匹配,那个 Stream 不会发出任何值,而且 Register 按钮保持不活动状态,但用户不会收到任何错误消息以帮助他了解缘由。
另外一种解决方案包括扩展 confirmPassword 流的处理,以下所示:
Stream<String> get confirmPassword => _passwordConfirmController.stream.transform(validatePassword)
.doOnData((String c){
// If the password is accepted (after validation of the rules)
// we need to ensure both password and retyped password match
if (0 != _passwordController.value.compareTo(c)){
// If they do not match, add an error
_passwordConfirmController.addError("No Match");
}
});
复制代码
一旦从新输入密码验证经过,它就会被 Stream 发出,而且,经过使用 doOnData,咱们能够直接获取此发出的值并将其与 password 流的值进行比较。 若是二者不匹配,咱们如今能够发送错误消息。
如今让咱们在解释前先看一下 RegistrationForm 的代码 :
class RegistrationForm extends StatefulWidget {
@override
_RegistrationFormState createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
RegistrationFormBloc _registrationFormBloc;
@override
void initState() {
super.initState();
_registrationFormBloc = RegistrationFormBloc();
}
@override
void dispose() {
_registrationFormBloc?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Form(
child: Column(
children: <Widget>[
StreamBuilder<String>(
stream: _registrationFormBloc.email,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'email',
errorText: snapshot.error,
),
onChanged: _registrationFormBloc.onEmailChanged,
keyboardType: TextInputType.emailAddress,
);
}),
StreamBuilder<String>(
stream: _registrationFormBloc.password,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'password',
errorText: snapshot.error,
),
obscureText: false,
onChanged: _registrationFormBloc.onPasswordChanged,
);
}),
StreamBuilder<String>(
stream: _registrationFormBloc.confirmPassword,
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
return TextField(
decoration: InputDecoration(
labelText: 'retype password',
errorText: snapshot.error,
),
obscureText: false,
onChanged: _registrationFormBloc.onRetypePasswordChanged,
);
}),
StreamBuilder<bool>(
stream: _registrationFormBloc.registerValid,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return RaisedButton(
child: Text('Register'),
onPressed: (snapshot.hasData && snapshot.data == true)
? () {
// launch the registration process
}
: null,
);
}),
],
),
);
}
}
复制代码
说明:
因为 RegisterFormBloc 仅供此表单使用,所以在此处初始化它是适合的。
每一个 TextField 都包装在 StreamBuilder <String> 中,以便可以响应验证过程的任何结果(请参阅 errorText:snapshot.error)
每次对 TextField 的内容进行修改时,咱们都会经过 onChanged 发送输入到 BLoC 进行验证:_registrationFormBloc.onEmailChanged(电子邮件输入的状况)
对于RegisterButton,也包含在 StreamBuilder <bool> 中。
就这样! 表单中没有任何业务规则,这意味着能够更改规则而无需对表单进行任何修改,这样很是好!
有些时候,对于一个 Widget,根据它是否存在于某一个集合中来驱动其行为是一件有趣的事。
对于本文的最后一个例子,我将考虑如下场景:
应用程序处理商品;
用户能够选择放入购物车的商品;
一件商品只能放入购物车一次;
存放在购物车中的商品能够从购物车中移除;
一旦被移除,就能够将其再次添加到购物车中。
对于此例子,每一个商品将显示为一个按钮,该按钮如何显示将取决于该商品是否存在于购物车中。 若是该商品没有添加到购物车中,按钮将容许用户将其添加到购物车中。 若是商品已经被添加到购物车中,该按钮将容许用户将其从购物车中删除。
为了更好地说明 “Part of” 模式,我将考虑如下架构:
购物页面将显示全部可能的商品的列表;
购物页面中的每一个商品都会显示一个按钮,用于将商品添加到购物车或将其从购物车中删除,具体取决于其是否存在于在购物车中;
若是购物页面中的商品被添加到购物车中,其按钮将自动更新以容许用户将其从购物车中删除(反之亦然),而无需从新构建购物页面
另外一个页面,购物车页,将列出购物车中的全部商品;
能够今后页面中删除购物车中的任何商品。
边注
Part Of 这个名字是我我的取的名字。 这不是一个官方名称。
正如您如今能够想象的那样,咱们须要考虑一个专门用于处理全部商品的列表,以及存在于购物车中的商品的 BLoC。
这个BLoC可能以下所示:
class ShoppingBloc implements BlocBase {
// List of all items, part of the shopping basket
Set<ShoppingItem> _shoppingBasket = Set<ShoppingItem>();
// Stream to list of all possible items
BehaviorSubject<List<ShoppingItem>> _itemsController = BehaviorSubject<List<ShoppingItem>>();
Stream<List<ShoppingItem>> get items => _itemsController;
// Stream to list the items part of the shopping basket
BehaviorSubject<List<ShoppingItem>> _shoppingBasketController = BehaviorSubject<List<ShoppingItem>>(seedValue: <ShoppingItem>[]);
Stream<List<ShoppingItem>> get shoppingBasket => _shoppingBasketController;
@override
void dispose() {
_itemsController?.close();
_shoppingBasketController?.close();
}
// Constructor
ShoppingBloc() {
_loadShoppingItems();
}
void addToShoppingBasket(ShoppingItem item){
_shoppingBasket.add(item);
_postActionOnBasket();
}
void removeFromShoppingBasket(ShoppingItem item){
_shoppingBasket.remove(item);
_postActionOnBasket();
}
void _postActionOnBasket(){
// Feed the shopping basket stream with the new content
_shoppingBasketController.sink.add(_shoppingBasket.toList());
// any additional processing such as
// computation of the total price of the basket
// number of items, part of the basket...
}
//
// Generates a series of Shopping Items
// Normally this should come from a call to the server
// but for this sample, we simply simulate
//
void _loadShoppingItems() {
_itemsController.sink.add(List<ShoppingItem>.generate(50, (int index) {
return ShoppingItem(
id: index,
title: "Item $index",
price: ((Random().nextDouble() * 40.0 + 10.0) * 100.0).roundToDouble() /
100.0,
color: Color((Random().nextDouble() * 0xFFFFFF).toInt() << 0)
.withOpacity(1.0),
);
}));
}
}
复制代码
惟一可能须要解释的方法是 _postActionOnBasket() 方法。 每次在购物车中添加或删除项目时,咱们都须要“更新” _shoppingBasketController Stream 的内容,以便通知全部正在监听此 Stream 的 Widgets 并可以更新或从新构建页面。
此页面很是简单,只显示全部的商品。
class ShoppingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
ShoppingBloc bloc = BlocProvider.of<ShoppingBloc>(context);
return SafeArea(
child: Scaffold(
appBar: AppBar(
title: Text('Shopping Page'),
actions: <Widget>[
ShoppingBasket(),
],
),
body: Container(
child: StreamBuilder<List<ShoppingItem>>(
stream: bloc.items,
builder: (BuildContext context,
AsyncSnapshot<List<ShoppingItem>> snapshot) {
if (!snapshot.hasData) {
return Container();
}
return GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0,
),
itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) {
return ShoppingItemWidget(
shoppingItem: snapshot.data[index],
);
},
);
},
),
),
));
}
}
复制代码
说明:
AppBar 显示一个按钮:
显示出如今购物车中的商品的数量
单击时将用户重定向到购物车页面
商品列表使用 GridView 构建,包含在 StreamBuilder <List <ShoppingItem >> 中
每一个商品对应一个 ShoppingItemWidget
此页面与商品列表(ShoppingPage)很是类似,只是 StreamBuilder 如今正在侦听由 ShoppingBloc 公开的 _shoppingBasket 流的变化。
Part Of 模式依赖于这两个元素的组合:
让咱们看看他们如何一块儿工做......
ShoppingItemBloc 由每一个 ShoppingItemWidget 实例化,赋予它 “身份”。
此 BLoC 监听 ShoppingBasket 流的全部变化,并检测特定商品是不是存在于购物车中。
若是是,它会发出一个布尔值(= true),此值将被 ShoppingItemWidget 捕获,也就知道它是否存在于购物车中。
这是 BLoC 的代码:
class ShoppingItemBloc implements BlocBase {
// Stream to notify if the ShoppingItemWidget is part of the shopping basket
BehaviorSubject<bool> _isInShoppingBasketController = BehaviorSubject<bool>();
Stream<bool> get isInShoppingBasket => _isInShoppingBasketController;
// Stream that receives the list of all items, part of the shopping basket
PublishSubject<List<ShoppingItem>> _shoppingBasketController = PublishSubject<List<ShoppingItem>>();
Function(List<ShoppingItem>) get shoppingBasket => _shoppingBasketController.sink.add;
// Constructor with the 'identity' of the shoppingItem
ShoppingItemBloc(ShoppingItem shoppingItem){
// Each time a variation of the content of the shopping basket
_shoppingBasketController.stream
// we check if this shoppingItem is part of the shopping basket
.map((list) => list.any((ShoppingItem item) => item.id == shoppingItem.id))
// if it is part
.listen((isInShoppingBasket)
// we notify the ShoppingItemWidget
=> _isInShoppingBasketController.add(isInShoppingBasket));
}
@override
void dispose() {
_isInShoppingBasketController?.close();
_shoppingBasketController?.close();
}
}
复制代码
此 Widget 负责:
建立 一个 ShoppingItemBloc 实例并将本身的商品标识传递给BLoC
监听 ShoppingBasket 内容的任何变化并将其转移到BLoC
监听 ShoppingItemBloc 以判断它是否存在于购物车中
显示相应的按钮(添加/删除),具体取决于它是否存在于购物车中
响应按钮的用户操做
让咱们看看它是如何工做的(解释在代码中给出)。
class ShoppingItemWidget extends StatefulWidget {
ShoppingItemWidget({
Key key,
@required this.shoppingItem,
}) : super(key: key);
final ShoppingItem shoppingItem;
@override
_ShoppingItemWidgetState createState() => _ShoppingItemWidgetState();
}
class _ShoppingItemWidgetState extends State<ShoppingItemWidget> {
StreamSubscription _subscription;
ShoppingItemBloc _bloc;
ShoppingBloc _shoppingBloc;
@override
void didChangeDependencies() {
super.didChangeDependencies();
// As the context should not be used in the "initState()" method,
// prefer using the "didChangeDependencies()" when you need
// to refer to the context at initialization time
_initBloc();
}
@override
void didUpdateWidget(ShoppingItemWidget oldWidget) {
super.didUpdateWidget(oldWidget);
// as Flutter might decide to reorganize the Widgets tree
// it is preferable to recreate the links
_disposeBloc();
_initBloc();
}
@override
void dispose() {
_disposeBloc();
super.dispose();
}
// This routine is reponsible for creating the links
void _initBloc() {
// Create an instance of the ShoppingItemBloc
_bloc = ShoppingItemBloc(widget.shoppingItem);
// Retrieve the BLoC that handles the Shopping Basket content
_shoppingBloc = BlocProvider.of<ShoppingBloc>(context);
// Simple pipe that transfers the content of the shopping
// basket to the ShoppingItemBloc
_subscription = _shoppingBloc.shoppingBasket.listen(_bloc.shoppingBasket);
}
void _disposeBloc() {
_subscription?.cancel();
_bloc?.dispose();
}
Widget _buildButton() {
return StreamBuilder<bool>(
stream: _bloc.isInShoppingBasket,
initialData: false,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
return snapshot.data
? _buildRemoveFromShoppingBasket()
: _buildAddToShoppingBasket();
},
);
}
Widget _buildAddToShoppingBasket(){
return RaisedButton(
child: Text('Add...'),
onPressed: (){
_shoppingBloc.addToShoppingBasket(widget.shoppingItem);
},
);
}
Widget _buildRemoveFromShoppingBasket(){
return RaisedButton(
child: Text('Remove...'),
onPressed: (){
_shoppingBloc.removeFromShoppingBasket(widget.shoppingItem);
},
);
}
@override
Widget build(BuildContext context) {
return Card(
child: GridTile(
header: Center(
child: Text(widget.shoppingItem.title),
),
footer: Center(
child: Text('${widget.shoppingItem.price} €'),
),
child: Container(
color: widget.shoppingItem.color,
child: Center(
child: _buildButton(),
),
),
),
);
}
}
复制代码
下图显示了全部部分如何协同工做。
这是又一篇长文章,我原本但愿我能简短一点,但我认为,为了阐述得更清楚,这么长也是值得的。
正如我在简介中所说的,我我的在个人开发中常用这些“模式”。 这让我节省了大量的时间和精力; 个人代码更易读,更容易调试。
此外,它有助于将业务与视图分离。
颇有可能其余的方法也能够作到这些,甚至是更好的实现方式,但它对我是有用的,这就是我想与你分享的一切。
请继续关注新文章,同时祝您编程愉快。