原文:Reactive Programming - Streams - BLoC - Practical Use Cases 是作者 Didier Boelens 為 Reactive Programming - Streams - BLoC 寫的后續(xù)
閱讀本文前建議先閱讀前篇狼忱,前篇中文翻譯有兩個(gè)版本:
[譯]Flutter響應(yīng)式編程:Streams和BLoC by JarvanMo
忠于原作的版本Flutter中如何利用StreamBuilder和BLoC來控制Widget狀態(tài) by 吉原拉面
省略了一些初級概念衔肢,補(bǔ)充了一些個(gè)人解讀
前言
在了解 BLoC, Reactive Programming 和 Streams 概念后度陆,我又花了些時(shí)間繼續(xù)研究,現(xiàn)在非常高興能夠與你們分享一些我經(jīng)常使用并且個(gè)人覺得很有用的模式(至少我是這么認(rèn)為的)。這些模式為我節(jié)約了大量的開發(fā)時(shí)間曲管,并且讓代碼更加易讀和調(diào)試判沟。
目錄
(由于原文較長唾糯,翻譯發(fā)布時(shí)進(jìn)行了分割)
BlocProvider 性能優(yōu)化
結(jié)合 StatefulWidget 和 InheritedWidget 兩者優(yōu)勢構(gòu)建 BlocProviderBLoC 的范圍和初始化
根據(jù) BLoC 的使用范圍初始化 BLoC事件與狀態(tài)管理
基于事件(Event) 的狀態(tài) (State) 變更響應(yīng)表單驗(yàn)證
根據(jù)表單項(xiàng)驗(yàn)證來控制表單行為 (范例中包含了表單中常用的密碼和重復(fù)密碼比對)Part Of 模式
允許組件根據(jù)所處環(huán)境(是否在某個(gè)列表/集合/組件中)調(diào)整自身的行為
文中涉及的完整代碼可在 GitHub 查看。
3. 事件與狀態(tài)管理(Event - State)
有時(shí)侯需要我們編碼實(shí)現(xiàn)一些棘手的業(yè)務(wù)流程淮摔,這些流程可能會(huì)由串行或并行私沮、耗時(shí)長短不一、同步或異步的子流程構(gòu)成的噩咪,很可能每個(gè)子流程的處理結(jié)果也是千變?nèi)f化的顾彰,而且還可能需要根據(jù)其處理進(jìn)度或狀態(tài)進(jìn)行視圖更新。
而本文中「事件與狀態(tài)管理」解決方案的目的就是讓處理這種復(fù)雜的業(yè)務(wù)流程變得更簡單胃碾。
方案是基于以下流程和規(guī)則的:
- 發(fā)出某個(gè)事件
- 該事件觸發(fā)一些動(dòng)作(action)涨享,這些動(dòng)作會(huì)導(dǎo)致一個(gè)或多個(gè)狀態(tài)產(chǎn)生/變更
- 這些狀態(tài)又觸發(fā)其它事件,或者產(chǎn)生/變更為其它狀態(tài)
- 然后這些事件又根據(jù)狀態(tài)的變更情況仆百,觸發(fā)其它動(dòng)作
- 等等…
為了更好的展示這些概念厕隧,我還舉了兩個(gè)具體的例子:
-
應(yīng)用初始化 (Application initialization)
很多時(shí)候我們都需要運(yùn)行一系列動(dòng)作來初始化 App, 這些動(dòng)作可能是與服務(wù)器的交互相關(guān)聯(lián)的 (例如:獲取并加載一些數(shù)據(jù))俄周。而且在初始化過程中吁讨,可能還需要顯示進(jìn)度條及載入動(dòng)畫讓用戶能耐心等待。
-
用戶身份驗(yàn)證 (Authentication)
在 App 啟動(dòng)后需要用戶登錄或注冊峦朗,用戶成功登錄后建丧,將跳轉(zhuǎn)(重定向)到 App 的主頁面; 而用戶注銷則將跳轉(zhuǎn)(重定向)到驗(yàn)證頁面波势。
為了應(yīng)對所有的可能翎朱,我們將管理一系列的事件,而這些事件可能是在 App 中任何地方觸發(fā)的尺铣,這使得事件和狀態(tài)的管理異常復(fù)雜拴曲,所幸我們可以借助結(jié)合了 BlocEventStateBuider 的 BlocEventState 類大大降低事件和狀態(tài)管理的難度。
3.1. BlocEventState 抽象類
BlocEventState 背后的邏輯是將 BLoC 定義成這樣一套機(jī)制:
- 接收事件(event)作為輸入
- 當(dāng)新的事件觸發(fā)(輸入)時(shí)凛忿,調(diào)用一個(gè)對應(yīng)的事件處理器 eventHandler
- 事件處理器(eventHandler)負(fù)責(zé)根據(jù)事件(event)采用適當(dāng)?shù)奶幚?em>(actions)后澈灼,拋出一個(gè)或多個(gè)狀態(tài)(State)作為響應(yīng)
如下圖所示:
定義 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();
}
}
如代碼所示,我們定義的其實(shí)是一個(gè)抽象類店溢,是需要擴(kuò)展實(shí)現(xiàn)的叁熔,實(shí)現(xiàn)的重點(diǎn)就是定義 eventHandler 這個(gè)方法的具體行為。
當(dāng)然我們還可以看到:
- Sink (代碼中的 emitEvent) 作為事件 Event 的輸入入口
- Stream (代碼中的 state) 監(jiān)聽已發(fā)出的狀態(tài) State(s) 作為狀態(tài)的輸出出口
在這個(gè)類初始化時(shí)(參考代碼中 Constructor 部分):
- 需要提供初始狀態(tài) initialState
- 創(chuàng)建了一個(gè) StreamSubscription 用來監(jiān)聽輸入的事件 (Events) 并:
- 將事件分配給事件處理器 eventHandler
- 拋出結(jié)果 state(s)
3.2. BlocEventState 的擴(kuò)展實(shí)現(xiàn)
下方的模板代碼就是基于擴(kuò)展 BlocEventStateBase 抽象類實(shí)現(xiàn)了一個(gè)具體的 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();
}
}
模板代碼會(huì)報(bào)錯(cuò)逞怨,請不要擔(dān)心者疤,這是正常的…因?yàn)槲覀冞€沒有定義 BlocState.notInitialized()…后面會(huì)給出的。
這個(gè)模板只是在初始化時(shí)簡單地給出了一個(gè)初始狀態(tài) initialState叠赦,并覆寫了 eventHandler 方法驹马。
還需要注意的是革砸,我們使用了異步生成器 (asynchronous generator)語法:async* 和 yield
使用 async* 修飾符可將某個(gè)方法標(biāo)記為一個(gè)異步生成器(asynchronous generator)方法,比如上面的代碼中每次調(diào)用 eventHandler 方法內(nèi) yield 語句時(shí)糯累,它都會(huì)把 yield 后面的表達(dá)式結(jié)果添加到輸出 Stream 中算利。
如果我們需要通過一系列動(dòng)作觸發(fā)一系列States(后面會(huì)在范例中看到),這一點(diǎn)特別有用泳姐。
有關(guān)異步生成器的其他詳細(xì)信息效拭,可參考 這篇文章。
3.3. BlocEvent 和 BlocState
你可能注意到了胖秒,我們還定義了 BlocEvent 和 BlocState 兩個(gè)抽象類缎患,這兩個(gè)抽象類都是要根據(jù)實(shí)際情況,也就是在實(shí)際業(yè)務(wù)場景中根據(jù)你想要觸發(fā)的事件和拋出的狀態(tài)來具體擴(kuò)展實(shí)現(xiàn)的阎肝。
3.4. BlocEventStateBuilder 組件
這個(gè)模式的最后一部分就是 BlocEventStateBuilder 組件了挤渔,這個(gè)組件可以根據(jù) BlocEventState 拋出的 State(s)作出視圖層面的響應(yīng)。
代碼如下:
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);
},
);
}
}
其實(shí)這個(gè)組件除了一個(gè) StreamBuilder 外沒啥特別的风题,這個(gè) StreamBuilder 的作用就是每當(dāng)有新的 BlocState 拋出后判导,將其作為新的參數(shù)值調(diào)用 builder 方法。
好了沛硅,這些就是這個(gè)模式的全部構(gòu)成眼刃,接下來我們看看可以用它們來做些啥…
3.5. 事件與狀態(tài)管理例1: 應(yīng)用初始化(Application Initialization)
第一個(gè)例子演示了 App 在啟動(dòng)時(shí)執(zhí)行某些任務(wù)的情況。
一個(gè)常見的場景就是游戲的啟動(dòng)畫面摇肌,也稱 Splash 界面(不管是不是動(dòng)畫的)擂红,在顯示真正的游戲主界面前,游戲應(yīng)用會(huì)從服務(wù)器獲取一些文件围小、檢查是否需要更新篮条、嘗試與系統(tǒng)的「游戲中心」通訊等等;而且在完成初始化前吩抓,為了不讓用戶覺得應(yīng)用啥都沒做,可能還會(huì)顯示進(jìn)度條赴恨、定時(shí)切換顯示一些圖片等疹娶。
我給出的實(shí)現(xiàn)是非常簡單的,只顯示了完成百分比的伦连,你可以根據(jù)自己的需要非常容易地進(jìn)行擴(kuò)展雨饺。
首先要做的就是定義事件和狀態(tài)…
3.5.1. 定義事件: ApplicationInitializationEvent
作為例子,這里我只考慮了 2 個(gè)事件:
- start:觸發(fā)初始化處理過程
- stop:用于強(qiáng)制停止初始化過程
它們的定義如下:
class ApplicationInitializationEvent extends BlocEvent {
final ApplicationInitializationEventType type;
ApplicationInitializationEvent({
this.type: ApplicationInitializationEventType.start,
}) : assert(type != null);
}
enum ApplicationInitializationEventType {
start,
stop,
}
3.5.2. 定義狀態(tài): ApplicationInitializationState
ApplicationInitializationState 類將提供與初始化過程相關(guān)的信息惑淳。
同樣作為例子额港,這里我只考慮了:
- 2 個(gè) flag:
- isInitialized 用來標(biāo)識(shí)初始化是否完成
- isInitializing用來知曉我們是否處于初始化過程中
- 進(jìn)度完成率 prograss
代碼如下:
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,
);
}
}
3.5.3. 實(shí)現(xiàn) BLoC: ApplicationInitializationBloc
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();
}
}
}
說明:
- 當(dāng)接收到 ApplicationInitializationEventType.start 事件時(shí)歧焦,進(jìn)度完成率 prograss 將從
0
到100
開始計(jì)數(shù)(每次步進(jìn)10
)移斩,而且未到100
時(shí)每次都將通過 yield 拋出一個(gè)新狀態(tài)(state)告知初始化正在進(jìn)行(isInitializing =true
)及完成進(jìn)度 prograss 具體的值 - 當(dāng)接收到 ApplicationInitializationEventType.stop 事件時(shí),會(huì)認(rèn)為初始化已經(jīng)完成。
- 如你所見向瓷,我在循環(huán)過程中加了些延遲(delay)肠套,目的是演示 Future的適用場景(如從服務(wù)器獲取數(shù)據(jù))
3.5.4. 組合使用
現(xiàn)在,剩下的事情就是把代表進(jìn)度完成率的計(jì)數(shù)器顯示到假的 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}%');
},
),
),
),
),
);
}
}
說明:
- 在 App 中猖任,ApplicationInitializationBloc 并不是任何組件都需要用到你稚,所以只在一個(gè) StatefulWidget 中初始化(實(shí)例化)了該 BLoC
- 直接發(fā)出 ApplicationInitializationEventType.start 事件來觸發(fā) eventHandler
- 每次 ApplicationInitializationState 被拋出,都會(huì)更新文字內(nèi)容
- 初始化過程完成后朱躺,跳轉(zhuǎn)(重定向)到了 Home 界面
小技巧
由于無法直接跳轉(zhuǎn)到 Home 界面刁赖,在 builder 方法中,使用了WidgetsBinding.instance.addPostFrameCallback() 方法來請求 Flutter 在完成渲染后執(zhí)行跳轉(zhuǎn)长搀。參考 addPostFrameCallback()
3.6. 事件與狀態(tài)管理例2: 用戶身份驗(yàn)證(登錄與注銷)
在這個(gè)例子中宇弛,我考慮了如下場景:
- 如果用戶沒有登錄,則自動(dòng)顯示登錄/注冊(Authentication/Registration)界面
- 用戶提交登錄信息后盈滴,顯示一個(gè)代表正在處理的循環(huán)進(jìn)度指示器(轉(zhuǎn)圈圈)
- 一旦用戶登錄成功涯肩,將跳轉(zhuǎn)到 Home 界面
- 在 App 任何地方,用戶都可能注銷
- 如果用戶注銷巢钓,將自動(dòng)跳轉(zhuǎn)到登錄(Authentication)界面
當(dāng)然以其它編程方式也可以實(shí)現(xiàn)這些功能病苗,但以 BLoC 的方式來實(shí)現(xiàn)可能更簡單。
下圖解釋了將要實(shí)現(xiàn)的方案流程:
中間跳轉(zhuǎn)頁面DecisionPage將負(fù)責(zé)自動(dòng)將用戶重定向到 Authentication 界面或 Home 界面症汹,具體到哪個(gè)界面取決于用戶的登錄狀態(tài)硫朦。當(dāng)然 DecisionPage 不會(huì)顯示給用戶,也不應(yīng)該將其視為一個(gè)真正的頁面背镇。
同樣首先要做的是定義一些事件和狀態(tài)…
3.6.1. 定義事件: AuthenticationEvent
作為例子咬展,我只考慮了2個(gè)事件:
- login:用戶成功登錄時(shí)會(huì)發(fā)出該事件
- logout:用戶注銷時(shí)會(huì)發(fā)出該事件
它們的定義如下:
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.6.2. 定義狀態(tài): AuthenticationState
AuthenticationState 類將提供與驗(yàn)證過程相關(guān)的信息。
同樣作為例子瞒斩,我只考慮了:
- 3 個(gè) flag:
- isAuthenticated 用來標(biāo)識(shí)驗(yàn)證是否完成
- isAuthenticating 用來知曉是否處于驗(yàn)證過程中
- hasFailed 用來表示身份是否驗(yàn)證失敗
- 經(jīng)過身份驗(yàn)證后的用戶名:name
代碼如下:
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,
);
}
}
3.6.3. 實(shí)現(xiàn) BLoC: AuthenticationBloc
BLoC 將基于事件類型來處理具體的身份驗(yàn)證過程破婆。
代碼如下:
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();
}
}
}
說明:
- 當(dāng)接收到 AuthenticationEventLogin事件時(shí),會(huì)通過 yield 拋出一個(gè)新狀態(tài)(state)告知身份驗(yàn)證正在進(jìn)行(isAuthenticating =
true
) - 當(dāng)身份驗(yàn)證一旦完成胸囱,會(huì)拋出另一個(gè)新的狀態(tài)(state)告知已經(jīng)完成了
- 當(dāng)接收到AuthenticationEventLogout事件時(shí)祷舀,會(huì)拋出一個(gè)新狀態(tài)(state)告知用戶已經(jīng)不在是已驗(yàn)證狀態(tài)
3.6.4. 登錄頁面: AuthenticationPage
如你所見,為了便于說明烹笔,這個(gè)頁面并沒有做的很復(fù)雜裳扯。
代碼及說明如下:
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,
);
},
),
),
),
),
);
}
}
說明:
- 第 11 行:在頁面中獲取 AuthenticationBloc
- 第 24 ~ 70 行:監(jiān)聽被拋出的 AuthenticationState:
- 如果正在驗(yàn)證過程中,會(huì)顯示循環(huán)進(jìn)度指示器(轉(zhuǎn)圈圈)谤职,告知用戶正在處理中饰豺,并阻止用戶訪問到其它頁面(第25 ~ 27 行)
- 如果驗(yàn)證成功,顯示一個(gè)空的 Container允蜈,即不顯示任何內(nèi)容 (第 29 ~ 31 行)
- 如果用戶還沒有登錄冤吨,顯示2個(gè)按鈕蒿柳,可模擬登錄成功和失敗的情況
- 當(dāng)點(diǎn)擊其中一個(gè)按鈕時(shí),會(huì)發(fā)出 AuthenticationEventLogin 事件以及一些參數(shù)(通常會(huì)被用于驗(yàn)證處理)
- 如果身份驗(yàn)證失敗锅很,顯示一條錯(cuò)誤消息(第 60 ~ 64 行)
好了其馏,沒啥別的事了,很簡單對不爆安?
小技巧
你肯定注意到了叛复,我把頁面包在了 WillPopScope 里面,這是因?yàn)樯矸蒡?yàn)證是必須的步驟扔仓,除非成功登錄(驗(yàn)證通過)褐奥,我不希望用戶使用 Android 設(shè)備提供的 Back 鍵來跳過驗(yàn)證訪問到其它頁面。
3.6.5. 中間跳轉(zhuǎn)頁面: DecisionPage
如前所述翘簇,我希望 App 根據(jù)用戶登錄狀態(tài)自動(dòng)跳轉(zhuǎn)到 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'));
});
}
}
提示
為了詳細(xì)解釋下面的問題撬码,我們先回溯下 Flutter 處理 Pages(也就是 路由Route)的方式,即使用 Navigator 對象來管理 Routes版保,而 Navigator 對象創(chuàng)建了一個(gè) Overlay 對象呜笑;這個(gè) Overlay 其實(shí)是包含多個(gè) OverlayEntry 的 Stack 對象,而每個(gè) OverlayEntry 都包含了一個(gè) Page彻犁;
當(dāng)我們通過 Navigator.of(context) 操作路由堆棧進(jìn)行壓入叫胁、彈出或替換時(shí),也會(huì)更新 Overlay 對象(也就是Stack 對象)汞幢,換句話說驼鹅,這些操作會(huì)導(dǎo)致 Stack 對象的重構(gòu);而 Stack 重構(gòu)時(shí)森篷,OverlayEntry (包括其內(nèi)容 Page)也會(huì)跟著重構(gòu)输钩;
結(jié)果就是:
當(dāng)我們通過 Navigator.of(context) 進(jìn)行路由操作后,所有其它頁面都會(huì)重構(gòu)仲智!
-
那么买乃,為啥我要把它實(shí)現(xiàn)為 StatefulWidget ?
為了能夠響應(yīng) AuthenticationState 任何變更,這個(gè) page 需要在 App 整個(gè)生命周期內(nèi)保留钓辆;
而根據(jù)上面的提示为牍,每次調(diào)用 Navigator.of(context) 后,這個(gè)頁面都會(huì)被重構(gòu)岩馍,因此也會(huì)重構(gòu) BlocEventStateBuilder ,毫無疑問 BlocEventStateBuilder 里面的 builder 方法也會(huì)被調(diào)用抖韩;
因?yàn)檫@個(gè) builder 方法是負(fù)責(zé)將用戶重定向到與 AuthenticationState 對應(yīng)的頁面蛀恩,重定向又要通過 Navigator.of(context) 來實(shí)現(xiàn)…明顯死循環(huán)了
所以為了防止這種情況發(fā)生,我們需要將「最后一個(gè)」 AuthenticationState 存起來茂浮,只有當(dāng)新的 AuthenticationState 與已存的不一樣時(shí)双谆,我們才進(jìn)行重定向處理壳咕;
而實(shí)現(xiàn)存儲(chǔ)就是利用 StatefulWidget 的特性,將「最后一個(gè)」 AuthenticationState 放到了 State 的 oldAuthenticationState 屬性中顽馋。
-
到底是怎么運(yùn)作的谓厘?
如上所訴,每當(dāng) AuthenticationState 被拋出時(shí)寸谜,BlocEventStateBuilder 會(huì)調(diào)用 builder 方法竟稳,根據(jù) isAuthenticated 標(biāo)識(shí),我們就知道具體將用戶重定向到哪個(gè)頁面熊痴。
小技巧
由于在 builder 中無法直接跳轉(zhuǎn)到其它界面他爸,我們使用了WidgetsBinding.instance.addPostFrameCallback() 方法來請求 Flutter 在完成渲染后執(zhí)行跳轉(zhuǎn)。
此外果善,除了 DecisionPage 需要在整個(gè)應(yīng)用生命周期保留之外诊笤,我們需要移除路由堆棧中重定向前所有其它已存在的頁面,所以我們使用了 Navigator.of(context).pushAndRemoveUntil(…) 來實(shí)現(xiàn)這一目的巾陕。參考 pushAndRemoveUntil()
3.6.6. 用戶注銷
為了讓用戶能夠注銷讨跟,可以創(chuàng)建一個(gè) LogOutButton,放到 App 中任何地方鄙煤。
這個(gè)按鈕只需要點(diǎn)擊后發(fā)出 AuthenticationEventLogout() 事件晾匠,這個(gè)事件會(huì)觸發(fā)如下的自動(dòng)處理動(dòng)作:
- 事件由 AuthenticationBloc 進(jìn)行處理
- 處理后拋出一個(gè) AuthentiationState(isAuthenticated =
false
) - 拋出的狀態(tài)將由DecisionPage 通過 BlocEventStateBuilder 進(jìn)行處理
- 最后將用戶重定向到 AuthenticationPage
按鈕代碼如下:
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());
},
);
}
}
3.6.7. 注入 AuthenticationBloc
由于需要 AuthenticationBloc 在應(yīng)用中任何頁面都可用,所以我們將其注入為 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(),
),
);
}
}