原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是做者 Didier Boelens 为 Reactive Programming - Streams - BLoC 写的后续html
阅读本文前建议先阅读前篇,前篇中文翻译有两个版本:react
[译]Flutter响应式编程:Streams和BLoC by JarvanMogit
较忠于原做的版本github
Flutter中如何利用StreamBuilder和BLoC来控制Widget状态 by 吉原拉面正则表达式
省略了一些初级概念,补充了一些我的解读编程
在了解 BLoC, Reactive Programming 和 Streams 概念后,我又花了些时间继续研究,如今很是高兴可以与你们分享一些我常用而且颇有用的模式(至少我是这么认为的)。这些模式为我节约了大量的开发时间,而且让代码更加易读和调试。后端
在这篇文章中我要分享的有:缓存
BlocProvider 性能优化性能优化
结合 StatefulWidget 和 InheritedWidget 二者优点构建 BlocProvider服务器
BLoC 的范围和初始化
根据 BLoC 的使用范围初始化 BLoC
事件与状态管理
基于事件(Event) 的状态 (State) 变动响应
表单验证
根据表单项验证来控制表单行为 (范例中包含了表单中经常使用的密码和重复密码比对)
Part Of 模式
容许组件根据所处环境(是否在某个列表/集合/组件中)调整自身的行为
文中涉及的完整代码可在 GitHub 查看。
我想先给你们介绍下我结合 InheritedWidget 实现 BlocProvider 的新方案,这种方式相比原来基于 StatefulWidget 实现的方式有性能优点。
以前我是基于一个常规的 StatefulWidget 来实现 BlocProvider 的,代码以下:
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 初始化时分配的内存资源在不须要时能够释放掉。
译者注
这个优势是单独基于 InheritedWidget 很难实现的,由于 InheritedWidget 没有提供 dispose 方法,而 Dart 语言又没有自带的析构函数
虽然这种方案运行起来没啥问题,但从性能角度却不是最优解。
这是由于 context.ancestorWidgetOfExactType() 是一个时间复杂度为 O(n) 的方法,为了获取符合指定类型的 ancestor ,它会沿着视图树从当前 context 开始逐步往上递归查找其 parent 是否符合指定类型。若是当前 context 和目标 ancestor 相距不远的话这种方式还能够接受,不然应该尽可能避免使用。
下面是 Flutter 中定义这个方法的源码:
@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
译者注
即在原来 StatefulWidget 的 child 外面再包了一个 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,在查找符合指定类型的 ancestor 时,咱们就能够调用 InheritedWidget 的实例方法 context.ancestorInheritedElementForWidgetOfExactType(),而这个方法的时间复杂度是 O(1),意味着几乎能够当即查找到知足条件的 ancestor。
Flutter 中该方法的定义源码体现了这一点:
@override InheritedElement ancestorInheritedElementForWidgetOfExactType(Type targetType) { assert(_debugCheckStateIsActiveForAncestorLookup()); final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType]; return ancestor; } 复制代码
固然这也是源于 Fluter Framework 缓存了全部 InheritedWidgets 才得以实现。
为何要用 ancestorInheritedElementForWidgetOfExactType 而不用 inheritFromWidgetOfExactType ?
由于 inheritFromWidgetOfExactType 不只查找获取符合指定类型的Widget,还将context 注册到该Widget,以便Widget发生变更后,context能够获取到新值;
这并非咱们想要的,咱们想要的仅仅就是符合指定类型的Widget(也就是 BlocProvider)而已。
Widget build(BuildContext context){ return BlocProvider<MyBloc>{ bloc: myBloc, child: ... } } 复制代码
Widget build(BuildContext context){
MyBloc myBloc = BlocProvider.of<MyBloc>(context);
...
}
复制代码
要回答「要在哪初始化 BLoC?」这个问题,须要先搞清楚 BLoC 的可用范围 (scope)。
在实际应用中,经常须要处理如用户鉴权、用户档案、用户设置项、购物篮等等须要在 App 中任何组件均可访问的数据或状态,这里总结了适用这种状况的两种 BLoC 方案:
这种方案使用了一个不在Widget视图树中的 Global 对象,实例化后可用供全部 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 后调用定义好的方法便可:
import 'global_bloc.dart'; class MyWidget extends StatelessWidget { @override Widget build(BuildContext context){ globalBloc.push('building MyWidget'); //调用 push 方法添加数据 return Container(); } } 复制代码
若是你想要一个惟一的、可从应用中任何组件访问的 BLoC 的话,这个方案仍是不错的,由于:
我也不知道具体是为啥,不少较真的人反对全局单例方案,因此…咱们再来看另外一种实现方案吧…
在 Flutter 中,包含全部页面的ancestor自己必须是 MaterialApp 的父级。 这是由于页面(或者说Route)实际上是做为全部页面共用的 Stack 中的一项,被包含在 OverlayEntry 中的。
换句话说,每一个页面都有本身独立于任何其它页面的 Buildcontext。这也解释了为啥不用任何技巧是没办法实现两个页面(或路由)之间数据共享的。
所以,必须将 BlocProvider 做为 MaterialApp 的父级才能实如今应用中任何位置均可使用 BLoC,以下所示:
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。举个例子,在一个 App 中有相似论坛的功能模块,在这个功能模块中咱们须要用到 BLoC 来实现:
显然咱们不须要将论坛的 BLoC 实现成全局可用,只需在涉及论坛的视图树中可用就好了。
那么可采用经过 BlocProvider将 BLoC 做为模块子树的根(父级)注入的方式,以下所示:
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.
注意
上面给出的并非最佳方案,由于每次 MyTree 重构(rebuild)时都会从新初始化 BLoC ,带来的结果是:
- 丢失 BLoC 中已经存在的数据内容
- 从新初始化BLoC 要占用 CPU 时间
在这个例子中更好的方式是使用 StatefulWidget ,利用其持久化 State 的特性解决上述问题,代码以下:
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 组件重构,也不会从新初始化 BLoC,而是直接使用以前的BLoC实例。
若是只在某一个组件 (Widget) 中使用 BLoC,只须要在该组件内构建 BLoC 实例便可。
有时侯须要咱们编码实现一些棘手的业务流程,这些流程可能会由串行或并行、耗时长短不1、同步或异步的子流程构成的,极可能每一个子流程的处理结果也是变幻无穷的,并且还可能须要根据其处理进度或状态进行视图更新。
而本文中「事件与状态管理」解决方案的目的就是让处理这种复杂的业务流程变得更简单。
方案是基于如下流程和规则的:
为了更好的展现这些概念,我还举了两个具体的例子:
应用初始化 (Application initialization)
不少时候咱们都须要运行一系列动做来初始化 App, 这些动做多是与服务器的交互相关联的 (例如:获取并加载一些数据)。并且在初始化过程当中,可能还须要显示进度条及载入动画让用户能耐心等待。
用户身份验证 (Authentication)
在 App 启动后须要用户登陆或注册,用户成功登陆后,将跳转(重定向)到 App 的主页面; 而用户注销则将跳转(重定向)到验证页面。
为了应对全部的可能,咱们将管理一系列的事件,而这些事件多是在 App 中任何地方触发的,这使得事件和状态的管理异常复杂,所幸咱们能够借助结合了 BlocEventStateBuider 的 BlocEventState 类大大下降事件和状态管理的难度。
BlocEventState 背后的逻辑是将 BLoC 定义成这样一套机制:
以下图所示:
定义 BlocEventState 的代码和说明以下:
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 这个方法的具体行为。
固然咱们还能够看到:
在这个类初始化时 (参考代码中 Constructor 部分):
下方的模板代码就是基于扩展 BlocEventStateBase 抽象类实现了一个具体的 BlocEventState 类:
bloc_event_state_template.dart
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 方法。
还须要注意的是,咱们使用了 异步生成器 (asynchronous generator) 语法:async* 和 yield
使用 async* 修饰符可将某个方法标记为一个 异步生成器(asynchronous generator) 方法,好比上面的代码中每次调用 eventHandler 方法内 yield 语句时,它都会把 yield 后面的表达式结果添加到输出 Stream 中。
若是咱们须要经过一系列动做触发一系列 States (后面会在范例中看到),这一点特别有用。
有关 异步生成器 的其余详细信息,可参考 这篇文章。
你可能注意到了,咱们还定义了 BlocEvent 和 BlocState 两个抽象类,这两个抽象类都是要根据实际状况,也就是在实际业务场景中根据你想要触发的事件和抛出的状态来具体 扩展实现 的。
这个模式的最后一部分就是 BlocEventStateBuilder 组件了,这个组件能够根据 BlocEventState 抛出的 State(s) 做出视图层面的响应。
代码以下:
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); }, ); } } 复制代码
其实这个组件除了一个 StreamBuilder 外没啥特别的,这个 StreamBuilder 的做用就是每当有新的 BlocState 抛出后,将其做为新的参数值调用 builder 方法。
好了,这些就是这个模式的所有构成,接下来咱们看看能够用它们来作些啥…
第一个例子演示了 App 在启动时执行某些任务的状况。
一个常见的场景就是游戏的启动画面,也称 Splash 界面(无论是否是动画的),在显示真正的游戏主界面前,游戏应用会从服务器获取一些文件、检查是否须要更新、尝试与系统的「游戏中心」通信等等;并且在完成初始化前,为了避免让用户以为应用啥都没作,可能还会显示进度条、定时切换显示一些图片等。
我给出的实现是很是简单的,只显示了完成百分比的,你能够根据本身的须要很是容易地进行扩展。
首先要作的就是定义事件和状态…
做为例子,这里我只考虑了 2 个事件:
它们的定义以下:
class ApplicationInitializationEvent extends BlocEvent { final ApplicationInitializationEventType type; ApplicationInitializationEvent({ this.type: ApplicationInitializationEventType.start, }) : assert(type != null); } enum ApplicationInitializationEventType { start, stop, } 复制代码
ApplicationInitializationState 类将提供与初始化过程相关的信息。
一样做为例子,这里我只考虑了:
代码以下:
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(); } } } 复制代码
说明:
0
到 100
开始计数(每次步进 10
),并且未到 100
时每次都将经过 yield 抛出一个新状态 (state) 告知初始化正在进行 (isInitializing = true
) 及完成进度 prograss 具体的值如今,剩下的事情就是把表明进度完成率的计数器显示到假的 Splash 界面上:
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){ // // Once the initialization is complete, let's move to another page // WidgetsBinding.instance.addPostFrameCallback((_){ Navigator.of(context).pushReplacementNamed('/home'); }); } return Text('Initialization in progress... ${state.progress}%'); }, ), ), ), ), ); } } 复制代码
说明:
小技巧
因为没法直接跳转到 Home 界面,在 builder 方法中,使用了WidgetsBinding.instance.addPostFrameCallback() 方法来请求 Flutter 在完成渲染后执行跳转。参考 addPostFrameCallback()
在这个例子中,我考虑了以下场景:
固然以其它编程方式也能够实现这些功能,但以 BLoC 的方式来实现可能更简单。
下图解释了将要实现的方案流程:
中间跳转页面 DecisionPage 将负责 自动 将用户重定向到 Authentication 界面或 Home 界面,具体到哪一个界面取决于用户的登陆状态。固然 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 {} 复制代码
AuthenticationState 类将提供与验证过程相关的信息。
一样做为例子,我只考虑了:
代码以下:
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(); } } } 复制代码
说明:
true
)如你所见,为了便于说明,这个页面并无作的很复杂。
代码及说明以下:
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 设备提供的 Back 键来跳过验证访问到其它页面。
如前所述,我但愿 App 根据用户登陆状态自动跳转到 AuthenticationPage 或 HomePage
代码及说明以下:
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 对象来管理 Routes,而 Navigator 对象建立了一个 Overlay 对象;这个 Overlay 实际上是包含多个 OverlayEntry 的 Stack 对象,而每一个 OverlayEntry 都包含了一个 Page;
当咱们经过 Navigator.of(context) 操做路由堆栈进行压入、弹出或替换时,也会更新 Overlay 对象(也就是Stack 对象),换句话说,这些操做会致使 Stack 对象的重构;而 Stack 重构时,OverlayEntry (包括其内容 Page)也会跟着重构;
结果就是:
当咱们经过 Navigator.of(context) 进行路由操做后,全部其它页面都会重构!
那么,为啥我要把它实现为 StatefulWidget ?
为了可以响应 AuthenticationState 任何变动,这个 page 须要在 App 整个生命周期内保留;
而根据上面的提示,每次调用 Navigator.of(context) 后,这个页面都会被重构,所以也会重构 BlocEventStateBuilder ,毫无疑问 BlocEventStateBuilder 里面的 builder 方法也会被调用;
由于这个 builder 方法是负责将用户重定向到与 AuthenticationState 对应的页面,重定向又要经过 Navigator.of(context) 来实现…明显死循环了
因此为了防止这种状况发生,咱们须要将「最后一个」 AuthenticationState 存起来,只有当新的 AuthenticationState 与已存的不同时,咱们才进行重定向处理;
而实现存储就是利用 StatefulWidget 的特性,将「最后一个」 AuthenticationState 放到了 State 的 oldAuthenticationState 属性中。
究竟是怎么运做的?
如上所诉,每当 AuthenticationState 被抛出时,BlocEventStateBuilder 会调用 builder 方法,根据 isAuthenticated 标识,咱们就知道具体将用户重定向到哪一个页面。
小技巧
因为在 builder 中没法直接跳转到其它界面,咱们使用了WidgetsBinding.instance.addPostFrameCallback() 方法来请求 Flutter 在完成渲染后执行跳转。
此外,除了 DecisionPage 须要在整个应用生命周期保留以外,咱们须要移除路由堆栈中重定向前全部其它已存在的页面,因此咱们使用了 Navigator.of(context).pushAndRemoveUntil(…) 来实现这一目的。参考 pushAndRemoveUntil()
为了让用户可以注销,能够建立一个 LogOutButton,放到 App 中任何地方。
这个按钮只须要点击后发出 AuthenticationEventLogout() 事件,这个事件会触发以下的自动处理动做:
false
)按钮代码以下:
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个 TextField (分别为电子邮箱email、密码password和重复密码 confirmPassword)以及一个按钮 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(); } } 复制代码
说明:
好了,咱们来深刻了解更多的细节…
你可能注意到了,这个 BLoC 类的代码有点特殊,是这样的:
class RegistrationFormBloc extends Object with EmailValidator, PasswordValidator implements BlocBase { ... } 复制代码
使用了 with 关键字代表这个类用到了 MIXINS (一种在另外一个类中重用类代码的方法),并且为了使用 with,这个类还须要基于 Object 类进行扩展。这些 mixins 包含了 email 和 password 各自的验证方式。
关于 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 获取输入,而后引用 Stream 的 transform 方法进行输入的处理,并将处理后的数据从新注入到初始的 Stream 中。
在上面的代码中,处理流程包括根据一个 正则表达式 检查输入的内容,若是匹配则将输入的内容从新注入到 stream 中;若是不匹配,则将错误信息注入给 stream
如前所述,若是验证成功,StreamTransformer 会把输入的内容从新注入回 Stream,具体是怎么运做的呢?
咱们先看看 Observable.combineLatest3() 这个方法,它在每一个 Stream 全都抛出至少一个值以前,并不会给出任何值
以下图所示:
true
值(见代码第 35 行)我在网上看到有不少关于密码与重复密码的验证问题,解决方案确定是有不少的,这里我针对其中两种说明下。
第一种解决方案的代码以下:
Stream<bool> get registerValid => Observable.combineLatest3( email, password, confirmPassword, (e, p, c) => (0 == p.compareTo(c)) ); 复制代码
这个解决方案只是在验证了两个密码以后,将它们进行比较,若是它们同样,则会抛出一个 true
值。
等下咱们会看到,Register 按钮是否可用是依赖于 registerValid stream 的,若是两个密码不同,registerValid stream 就不会抛出任何值,因此 Register 按钮依然是不可用状态。
可是,用户不会接收到任何错误提示信息,因此也不明白发生了什么。
另外一种方案是把 confirmPassword stream的处理方法进行了扩展,代码以下:
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"); } }); 复制代码
一旦 retype password 业务规则验证经过, 用户输入的内容会被 Stream 抛出,并调用 doOnData() 方法,在该方法中经过 _passwordController.value.compareTo() 获取是否与 password stream 中的数据同样,若是不同,咱们就可用添加错误提示了。
在解释说明前咱们先来看看 Form 组件的实现代码:
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, ); }), ], ), ); } } 复制代码
说明:
好了!可用看到在表单组件中,是看不到任何和业务规则相关的代码的,这意味着咱们能够随意修改业务规则,而不须要对表单组件自己进行任何修改,简直 excellent!
有时候,须要组件根据所处环境(是不是属于某个列表/集合/组件等)来驱动自身的行为,做为本文的最后一个范例,咱们将考虑以下场景:
在例子中,每一个商品都会显示一个按钮,这个按钮根据商品是不是在购物篮中决定其行为:
为了更好地说明 Part of 模式,我采用了如下的代码架构:
注意
「Part Of 模式」 这个名字是我本身取的,并非官方名称。
你可能已经想到了,咱们须要考虑让 BLoC 来处理全部商品的列表,以及 Shopping Basket 页面中的(已添加到购物篮中的)商品列表
这个 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 的组件就会收到变动通知,以便组件自身进行刷新或重建 (refresh/rebuild)
这个页面很简单,就是显示全部商品而已:
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], ); }, ); }, ), ), )); } } 复制代码
说明:
This page is very similar to the ShoppingPage except that the StreamBuilder is now listening to variations of the _shoppingBasket stream, exposed by the ShoppingBloc.
这个页面和 ShoppingPage 很是类似,只是其 StreamBuilder 监听对象是 ShoppingBloc 提供的 _shoppingBasket stream 的变动结果
Part Of 模式依赖于ShoppingItemWidget 和 ShoppingItemBloc两个元素的组合应用:
咱们来看看它们是怎么一块儿运做的…
ShoppingItemBloc 由每一个 ShoppingItemWidget 来实例化,并向其提供了自身的商品 ID (identity);
BLoC 将监听 ShoppingBasket stream 的变动结果,并检查具备特定 ID 的商品是否已在购物篮中;
若是已在购物篮中,BLoC 将抛出一个布尔值(=true
),对应 ID 的 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(); } } 复制代码
这个组件负责:
来看看具体的实现代码和说明:
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(), ), ), ), ); } } 复制代码
具体每部份的运做方式可参考下图
又一篇长文,我却是但愿可以少写点,可是我以为不少东西要解释清楚。
正如我在前言中说的,就我我的来讲这些「模式」我已经中在开发中常用了,它们帮我节省了大量的时间和精力,并且产出的代码更加易读和调试;此外还有助于业务和视图的解耦分离。
确定有大量其它方式也能够作到,甚至是更好的方式,可是本文中的模式对我来讲确实很实用,这就是为啥我想与你分享的缘由。
请继续关注新的文章,同时祝您编程愉快。
--全文完--