原文: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(), ), ), ), ); } } 複製代碼
具體每部份的運做方式可參考下圖
又一篇長文,我卻是但願可以少寫點,可是我以爲不少東西要解釋清楚。
正如我在前言中說的,就我我的來講這些「模式」我已經中在開發中常用了,它們幫我節省了大量的時間和精力,並且產出的代碼更加易讀和調試;此外還有助於業務和視圖的解耦分離。
確定有大量其它方式也能夠作到,甚至是更好的方式,可是本文中的模式對我來講確實很實用,這就是爲啥我想與你分享的緣由。
請繼續關注新的文章,同時祝您編程愉快。
--全文完--