很是感謝 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(),
),
),
),
);
}
}
複製代碼
下圖顯示了全部部分如何協同工做。
這是又一篇長文章,我原本但願我能簡短一點,但我認爲,爲了闡述得更清楚,這麼長也是值得的。
正如我在簡介中所說的,我我的在個人開發中常用這些「模式」。 這讓我節省了大量的時間和精力; 個人代碼更易讀,更容易調試。
此外,它有助於將業務與視圖分離。
頗有可能其餘的方法也能夠作到這些,甚至是更好的實現方式,但它對我是有用的,這就是我想與你分享的一切。
請繼續關注新文章,同時祝您編程愉快。