flutter 是由谷歌公司於 2015 年推出的移動 UI 框架。其選用 Dart 做爲開發語言,內置 Material Design 和 Cupertino 兩種設計風格 Widget 部件,使得它能夠快速在 ios、android 設備上構建高效、高品質、高流暢度的用戶 UI 界面。html
Flutter 在不少設計理念上參考了 React 的設計理念,所以在狀態管理庫的選擇和業務流處理上,也能夠選擇和 React 相似的解決方案,其中最具表明的即是 Redux。android
在聲明式 UI 組件開發的過程當中,一個 Flutter 組件呈現出樹狀結構,抽象出來大概長這樣: ios
能夠發現,這種結構下,咱們經過參數去傳遞一些數據,由父組件向子組件傳遞數據,子組件負責根據數據進行 UI 組件的渲染,這種數據流是自上而下流動的。編程
可是有時,咱們會須要在 app 的不一樣界面中共享應用程序的某些狀態或者是數據。若是他們沒有任何關係,咱們可能須要不少額外操做來作組件之間的通信,傳遞這些數據。這時候,咱們便須要一種獨立在組件以後的數據源和狀態庫,來管理這些公共狀態和數據。redux
在 Flutter 項目中,應用的狀態分爲短時狀態和應用狀態:api
短時狀態也稱局部狀態。好比一個 tab 選項卡中,當前被選中的 tab 對應的序列號;一個輸入框當前輸入的值均可以稱爲短時狀態(局部狀態)。在通常場景下,對於這類狀態,每每不是其餘組件所關心的,也不須要咱們幫助用戶記住這種狀態。即便應用重啓,這些狀態恢復到默認狀態,也不會對應用形成太大影響,這種狀態的丟失是能夠接受的。數組
應用狀態,也稱共享狀態,全局狀態等。最具表明性的,即是 loginFlag(一個用於標識用戶當前是否登陸的字段)了。這種狀態,每每影響了整個應用的邏輯和UI的渲染,由於用戶是否登陸決定咱們是否返回當前用戶的我的信息等。並且不少時候,登陸狀態一旦設置,咱們可能在一段時間內要記住這種狀態,即便應用重啓,這種狀態也不可丟失。相似這種的狀態和數據,便稱爲應用狀態。服務器
在 Flutter 中,可提供應用狀態管理的工具和第三方組件庫有不少,如:Redux, Provider, BloC, RxDart 等。此次記錄主要提供以下三種狀態庫的介紹及使用:markdown
咱們經過使用 Redux,BloC 及 Provider 從分別完成一個數據流的傳遞,來對比這三者的使用。app
實現的效果以下:
導入依賴
使用 Redux ,咱們須要先導入使用 Dart 編寫的 Redux 狀態庫,還須要導入用於鏈接 Flutter 應用和Redux狀態的連接庫flutter-redux:
在 pubspec.yml 文件中導入依賴, 並在命令行運行 flutter pub get
從遠程服務器獲取依賴:
設計狀態模型 Model
根據前面的需求概述,咱們的應用狀態根 AppState 中應至少包含下面兩個模塊的狀態:
* 全局狀態 => globleState 用於保存全局的 **應用狀態**
* 用戶狀態 => userState 用於保存用戶相關的**應用狀態**
複製代碼
生成狀態 UserState model 類
依據前面對應用狀態樹的設計,咱們首先完成 UserState Model 類的創建:
新建 UserState model 類:
/// model/user_model.dart
/// store user state
class UserState {
String name;
String email;
double age;
UserState({
@required this.name,
@required this.email,
@required this.age
});
}
複製代碼
在使用 Redux 進行狀態管理時,一般會須要給應用的狀態一些默認值,所以能夠經過命名構造函數爲 UserState 提供一個用於初始化的構造函數 initState:
/// model/user_model.dart
class UserState {
String name;
String email;
double age;
UserState({
@required this.name,
@required this.email,
@required this.age
});
UserState.initState(): name = 'redux', email = 'redux@gmail.com', age = 10;
}
複製代碼
經過構造方法,咱們即可以在合適的地方調用 initState 構造函數爲 UserState 類提供默認值。
使用過 Redux 的都知道,在 Redux 中,全部的狀態都由 Reducer 純函數生成,Reducer 函數經過接受新、舊狀態進行合併,生成新的狀態返回到狀態樹中。 爲了防止咱們上一次的狀態丟失,咱們應該將上一次的狀態記錄下來並與新狀態進行合併處理,所以咱們還須要在 UserState 類中添加一個 copy 方法用於狀態的合併:
關於純函數能夠參考函數式編程
/// model/user_model.dart
class UserState {
String name;
String email;
double age;
UserState({
@required this.name,
@required this.email,
@required this.age
});
UserState.initState(): name = 'redux', email = 'redux@gmail.com', age = 10;
UserState copyWith(UserModel userModel) {
return UserState(
name: userModel.name ?? this.name,
email: userModel.email ?? this.email,
age: userModel.age ?? this.age
);
}
}
複製代碼
咱們在類中編寫了一個 copyWith 方法,這個方法針對當前實例,接受用戶信息 user model, 經過判斷是否有新值傳入來決定是否返回老狀態。
這樣一個 UserState 的類便建立好了。
編寫 GlobalState, AppState model 類
與 UserState 相似,咱們快速完成 GlobalState, AppState 類
GlobalState model 類:
/// model/global_model.dart
import 'package:flutter/material.dart';
/// store global state
class GlobalState {
bool loginFlag;
GlobalState({
@required this.loginFlag
});
GlobalState.initState(): loginFlag = false;
GlobalState copyWith(loginFlag) {
return GlobalState(
loginFlag: loginFlag ?? this.loginFlag
);
}
}
複製代碼
App State model 類:
/// model/app_model.dart
import 'package:flutter_state/Redux/model/global_model.dart';
import 'package:flutter_state/Redux/model/user_model.dart';
/// APP global
class AppState {
UserState userState;
GlobalState globalState;
AppState({ this.userState, this.globalState });
AppState copyWith({
UserState userState,
GlobalState globalState,
}) {
return AppState(
userState: userState ?? this.userState,
globalState: globalState ?? this.globalState
);
}
}
複製代碼
創建 store 倉庫
接下里,咱們須要在項目根目錄中建立一個 store 文件夾,用於存放項目中所須要的 action 和 reducer 文件:
* - store
* - action.dart
* - reducer.dart
複製代碼
編寫 action
依據前面的需求,咱們在 action 中編寫項目中須要用到的 action 動做類。
// action.dart
import 'package:flutter_state/Redux/model/user_model.dart';
// User Action
enum UserAction {
SetUserInfo,
ClearUserInfo,
}
class SetUserInfo {
final UserModel userModel;
SetUserInfo(this.userModel);
}
class ClearUserInfo {}
// Global Action
enum GlobalAction {
SetLoginFlag,
LogoutSystem
}
class SetLoginFlag {
final bool loginFlag;
SetLoginFlag({ this.loginFlag });
}
class LogoutSystem {}
複製代碼
一般狀況下,一個 Action 動做由 Type 和 Payload 組成,Type 標識動做類型,Payload 做爲函數載體。 因爲 dart 靜態語言的一些特性,使用類來做爲數據載體,方便拓展和進行數據邏輯處理。 用類名、字符串仍是枚舉來定義你的 Action 動做類型,決定了你在 Reducer 中如何去判斷 Action 的動做類型進而進行相關的邏輯處理。實際業務中可根據業務場景靈活處理。
定義好相關的 action 動做後,咱們編寫對應的 reducer 函數。前面提到過,Reducer 函數經過接受新、舊狀態進行合併,生成新的狀態返回到狀態樹中:
// reducer.dart
...
import 'package:redux/redux.dart';
UserState userSetUpReducer(UserState userState, action) {
if (action is SetUserInfo) {
return userState.copyWith(action.userModel);
} else if (action is ClearUserInfo) {
return UserState.initState();
} else {
return userState;
}
}
GlobalState globalStatusReducer(GlobalState globalState, action) {
if (action is SetLoginFlag) {
return globalState.copyWith(action.loginFlag);
} else if (action is LogoutSystem) {
return GlobalState.initState();
} else {
return globalState;
}
}
複製代碼
上面的代碼中,分別定義了兩個純函數 userSetUpReducer, globalStatusReducer。他們的邏輯很是簡單,經過判斷 action 動做類型,對相應的 State 進行合併操做,生成新的狀態並返回。
因爲咱們使用`類`去做爲 Action 進行派發,所以在 reducer 中處理對應 action 時,可經過 is 來判斷類的類型
編寫頂層 appReducer 函數
完成子模塊 reducer 函數的編寫後,咱們須要完成組件狀態樹的頂層函數的 appReducer。appReducer 維護了咱們應用最頂層狀態,咱們在此處將對應的模塊狀態交給他們的 reducer 函數進行處理:
import 'package:flutter_state/Redux/model/app_model.dart';
import 'package:flutter_state/Redux/store/reducer.dart';
AppState appReducer(AppState appState, action) {
return appState.copyWith(
userState: userReducers(appState.userState, action),
globalState: globalReducers(appState.globalState, action),
);
}
複製代碼
appReducer 函數,接受 AppState,並經過 copyWith 方法,將 userState 和 globalState 狀態分別交由他們對應的 reducer 函數進行處理。
複製代碼
在應用中關聯 store
通常場景下,咱們只在業務最頂層維護一個全局的 store , 頂層的 store 經過 接受 reducer 函數來進行狀態的合併與分發處理
接下來,咱們在 應用入口處初始化 store 倉庫,並綁定到應用中:
// main.dart
...
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
// before
void main() {
runApp(MyApp())
};
// after
void main() {
final store = Store<AppState>(
appReducer,
initialState: AppState(
globalState: GlobalState.initState(),
userState: UserState.initState(),
)
);
runApp(
StoreProvider<AppState>(
store: store,
child: MyApp(),
)
);
}
...
複製代碼
上面的代碼,經過 Redux 中 store, 咱們初始化了一個 store 倉庫,在 initialState 裏咱們設置了應用的初始狀態。
以後咱們經過 flutter_redux 中 StoreProvider 方法,將 store 和 應用(MyApp)進行了關聯。
這樣咱們的 store 倉庫便導入完成了。
創建 UI 測試組件
新建 redux_perview 組件, 在其中完成視圖的編輯:
// redux_perview.dart
class ReduxPerviewPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
....
child: Column(
children: <Widget>[
Text('Redux Perview: ', style: TextStyle(fontSize: 40),),
SizedBox(height: 40,),
Text('Name: ', style: _textStyle),
Text('Email: ', style: _textStyle),
Text('Age: ', style: _textStyle),
]
),
}
}
複製代碼
Perview 負責對用戶的信息進行展現,當用戶沒有登陸時,該組件隱藏並使用 store 默認值。
新建 redux_trigger 組件,再其完成用於用戶輸入 UI 的綁定:
// redux_trigger
class ReduxTriggerPage extends StatelessWidget {
static final formKey = GlobalKey<FormState>();
final UserModel userModel = new UserModel();
Widget _loginForm (BuildContext context, Store) {
...
Column(
children: [
...
TextFormField(
decoration: InputDecoration(labelText: 'Name'),
onSaved: (input) => userModel.name = input,
),
...
]
)
}
@override
Widget build(BuildContext context) {
return _loginForm(context)
}
}
複製代碼
Trigger 組件接受用戶輸入的信息,提交到 store 倉庫。該組件在輸入完畢登陸成功以後,處於影藏狀態
此時運行效果以下:
在 ReduxPerviewPage 組件中使用狀態
接下來,咱們要在 UI 組件中綁定倉庫中的狀態。
Flutter_redux 爲咱們提供了兩個函數組件 StoreConnector 和 StoreBuilder,在文章的最後會針對這兩個方法的使用場景作進一步的介紹。
在此處,咱們使用 StoreBuilder 完成對 perview 展現頁面的綁定:
爲了防止嵌套過深,將 UI 部分抽離爲 _perviewWidget 方法
class ReduxPerviewPage extends StatelessWidget {
Widget _perviewWidget(BuildContext context, Store<AppState> store) {
...
UserState userState = store.state.userState;
...
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
...
Text('Name: ${userState.name}', style: _textStyle),
]
)
}
@override
Widget build(BuildContext context) {
return StoreBuilder<AppState>(
builder: (BuildContext context, Store<AppState> store) =>
store.state.globalState.loginFlag ? _perviewWidget(context, store) : Center(child: Text('請登陸'),)
);
}
}
複製代碼
上面的代碼中,咱們使用 StoreBuilder 的 builder 方法,拿到了上下文 context 和 store 的倉庫的狀態。經過邏輯判斷,將狀態傳入 _perviewWidget 完成頁面 Store 狀態至 UI 數據的綁定。
在 ReduxTrggier 改變頁面狀態
接下來咱們在 trigger 組件中提交 action 信息,來改變 state 中的狀態:
trigger 組件
class ReduxTriggerPage extends StatelessWidget {
static final formKey = GlobalKey<FormState>();
final UserModel userModel = new UserModel();
Widget _loginForm (BuildContext context, Store<AppState> store) {
return Center(
child: Container(
height: (MediaQuery.of(context).size.height - 120) / 2,
padding: EdgeInsets.symmetric(vertical: 20, horizontal: 30),
child: Form(
key: formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
TextFormField(
decoration: InputDecoration(labelText: 'Name'),
onSaved: (input) => userModel.name = input,
),
TextFormField(
decoration: InputDecoration(labelText: 'Email'),
onSaved: (input) => userModel.email = input,
),
TextFormField(
decoration: InputDecoration(labelText: 'Age'),
onSaved: (input) => userModel.age = double.parse(input),
),
FlatButton(
onPressed: () {
formKey.currentState.save();
// 提交 action 動做
StoreProvider.of<AppState>(context).dispatch(new SetLoginFlag(loginFlag: true));
StoreProvider.of<AppState>(context).dispatch(new SetUserInfo(userModel));
formKey.currentState.reset();
},
child: Text('遞交信息'),
color: Colors.blue,
textColor: Colors.white,
)
]
),
),
)
);
}
@override
Widget build(BuildContext context) {
return StoreBuilder<AppState>(
builder: (BuildContext context, Store<AppState> store) =>
store.state.globalState.loginFlag ? Text('') : _loginForm(context, store)
);
}
}
複製代碼
上面的代碼中,咱們在 StatelessWidget widget 無狀態組件中使用form 表單,對實例化的 userModel 對象進行賦值。 使用 Flutter_redux 提供的 StoreProvider 類,經過調用 of 靜態方法,即可以拿到 store 實例
拿到實例之後,即可經過 dispatch 方法發送對應的 action,redux 接受到 action 以後,便會交由 reducer 函數進行處理了。
StoreProvider 經過實現 InheritedWidget 機制實現,原理相似 redux 中的 context,當 store 發生改變的時候,StoreConnector 或者 StoreBuilder 狀態的獲得最新狀態,此時經過 StoreConnector 或 StoreBuilder 包裹的組件便都會獲得更新。
上面 redux_perview 例子,使用 StoreConnector 重構:
// redux_perview.dart
class ReduxPerviewPage extends StatelessWidget {
Widget _perviewWidget(BuildContext context, AppState appState) {
UserState userState = appState.userState;
return
...
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlutterLogo(),
Text('Redux Perview:', style: TextStyle(fontSize: 40),),
SizedBox(height: 40,),
Text('Name: ${userState.name}', style: _textStyle),
Text('Email: ${userState.email}', style: _textStyle),
Text('Age: ${userState.age}', style: _textStyle),
...
]
),
...
}
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, AppState>(
converter: (Store<AppState> store) => store.state,
builder: (BuildContext context, AppState appState) =>
appState.globalState.loginFlag ? _perviewWidget(context, appState) : Center(child: Text('請登陸'),)
);
}
}
複製代碼
StoreConnector 調用 converter 函數,將 Store 的映射爲 appState。 經過 StoreConnector,咱們能夠對 store 的參數進行處理,映射爲組件須要的狀態供組件進行使用。
StoreConnector 接受兩個泛型參數:
經過源碼,能夠看到第一個泛型,須要傳入 Store 聲明,第二參數能夠傳入你須要映射的類型。在 Flutter_redux 內部,converter 方法會在 initState 應用鉤子初始化的時候調用:
有了這層轉換,咱們即可以去作邏輯層的抽象,將咱們的實例映射成 viewModel,這樣即可進一步對邏輯層進行抽離,前面 redux_perview 例子,咱們作以下改造:
新建一個 PerviewViewModel 類:
class PerviewViewModel {
final UserState userModel;
final bool loginFlag;
final Function() clearUserInfo;
final Function() logout;
PerviewViewModel({
this.userModel,
this.loginFlag,
this.clearUserInfo,
this.logout
});
factory PerviewViewModel.create(Store<AppState> store) {
_clearUserInfo() {
store.dispatch(new ClearUserInfo());
}
_logout() {
store.dispatch(new LogoutSystem());
}
return PerviewViewModel(
userModel: store.state.userState,
loginFlag: store.state.globalState.loginFlag,
clearUserInfo: _clearUserInfo,
logout: _logout
);
}
}
複製代碼
在 previewModelView 中,咱們經過構造函數 create 傳入 store 實例,將 store 和 UI 相關的業務,所有抽離到 viewModel 當中。修改 converter 方法,將映射類型修改成 previewModelView:
...
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, PerviewViewModel>(
converter: (Store<AppState> store) => PerviewViewModel.create(store),
builder: (BuildContext context, PerviewViewModel model) =>
model.loginFlag ? _perviewWidget(context, model) : Center(child: Text('請登陸'),)
);
}
...
複製代碼
此時,咱們傳入的 UI 組件的數據變動爲 PerviewViewModel 實例,修改 _perviewWidget Ui 組件代碼:
...
Widget _perviewWidget(BuildContext context, PerviewViewModel model) {
...
FlutterLogo(),
Text('Redux Perview:', style: TextStyle(fontSize: 40),),
SizedBox(height: 40,),
Text('Name: ${model.userModel.name}', style: _textStyle),
Text('Email: ${model.userModel.email}', style: _textStyle),
Text('Age: ${model.userModel.age}', style: _textStyle),
FlatButton(
onPressed: () {
model.clearUserInfo();
model.logout();
},
child: Text('logout'),
color: Colors.grey,
textColor: Colors.black,
)
...
複製代碼
能夠發現,咱們經過 viewModel 的形式,將本來耦合在業務中邏輯代碼,抽離到 viewModel 中,針對於對 store 狀態的組合和邏輯代碼,就能夠與UI 組件進行解耦了。
這種模式在應對一些複雜的業務過程當中,能夠有效的幫助咱們去解綁 UI 和 store 層。將邏輯和 UI 分離,不只僅有利於咱們保持 UI 組件的簡潔,針對 viewModel 的邏輯,更方便了咱們去作業務的單元測試。在將來業務修改和移植的時候,也會更加清晰。
經過上面的案例能夠發現,使用 StoreConnector 能夠有效解耦業務,在一些簡單的場景下,使用 StoreConnector 可能讓代碼量增多。所以在使用 StoreConnector 仍是 StoreBuilder 上,我以爲在一些簡單場景下,咱們應該儘量抽取 UI 組件,對狀態的設計和數量要有必定控制,這時即可以使用 StoreBuilder 直接處理 store 相關邏輯。
可是對於一些複雜的業務場景,須要頻次對 store 進行操做的時候,爲了往後組件的複用及代碼清晰度,可使用 StoreConnector 對業務層進行抽象,這樣對往後的維護有很大好處。
在業務開發的過程當中,咱們能夠在 viemModel 中處理咱們的業務邏輯。可是對於一些異步問題,如 service api的調用,應該在何處進行呢?redux 基於一種通用的設計,解決了異步 Action 的調用問題,那即是加入 middleware(中間件)。
到底什麼是中間件呢?
中間件實際上是負責業務反作用,並處理業務邏輯相關工做(一般是異步的)的實體。 全部的 action 在到達 reducer 函數前都會通過中間件。每一箇中間件經過 next 函數,調用下一個中間件,並在 next 中傳遞 action 動做,進而完成由 中間件 => reducer => 中間件 的調度過程。
中間件使用示例
咱們結合兩個場景來演示中間件的使用。
前面在 redux_trggier 組件中,咱們經過直接觸發 setLoginFlag action 來完成了登陸狀態的設置。事實上在真實業務場景中,咱們應先對 setUserInfo action 中的入參進行必定的校驗後,在發送給服務器進行身份驗證。經過 http 請求拿到後臺返回的約定狀態碼後,再根據狀態碼判斷用戶是否登陸成功並觸發對應的 action
針對這個業務場景,咱們使用中間件來解決。
首先 action 中新增一些 新的 action 動做用與 action 的派發和相關業務:
// store/action.dart
// 用戶承載頁面入參的action
class ValidateUserLoginFields {
final UserModel userModel;
ValidateUserLoginFields(this.userModel);
}
// 用戶承載入參錯誤信息的action
class LoginFormFieldError {
final String nameErrorMessage;
final String emailErrorMessage;
final String ageErrorMessage;
LoginFormFieldError(
this.nameErrorMessage,
this.emailErrorMessage,
this.ageErrorMessage
);
}
// 用於發送用戶信息的 action
class FetchUserLogin {
final UserModel userModel;
FetchUserLogin(this.userModel);
}
// 用於清空錯誤信息的 action
class ClearUserLoginErrorMessage {}
複製代碼
咱們新增了上述的 4 個 action 來處理咱們的業務場景。修改 redux_trigger 組件,並新增 TriggerViewModel 來關聯咱們的組件和store:
// screen/redux_trigger.dart
class TriggerViewModel {
final String nameErrorMessage;
final String emailNameError;
final String ageNameError;
final bool loginFlag;
final Function(UserModel) fetchUserLogin;
TriggerViewModel({
this.nameErrorMessage,
this.emailNameError,
this.ageNameError,
this.loginFlag,
this.fetchUserLogin
});
factory TriggerViewModel.create(Store<AppState> store) {
_fetchUserLogin(UserModel userModel) {
// store.dispatch(new ClearUserLoginErrorMessage());
store.dispatch(new SetLoginFlag(loginFlag: true));
}
return TriggerViewModel(
nameErrorMessage: store.state.userState.nameErrorMessage,
emailNameError: store.state.userState.emailErrorMessage,
ageNameError: store.state.userState.ageErrorMessage,
loginFlag: store.state.globalState.loginFlag,
fetchUserLogin: _fetchUserLogin
);
}
}
複製代碼
修改 redux_trigger build 方法,並在 UI 中增長錯誤提示組件:
...
model.emailNameError.isNotEmpty ? Text(model.emailNameError, style: textStyle) : Container(),
TextFormField(
decoration: InputDecoration(labelText: 'Age'),
onSaved: (input) => userModel.age = input,
),
model.ageNameError.isNotEmpty ? Text(model.ageNameError, style: textStyle) : Container(),
FlatButton(
onPressed: () {
formKey.currentState.save();
model.fetchUserLogin(userModel);
// formKey.currentState.reset();
},
child: Text('遞交信息'),
color: Colors.blue,
textColor: Colors.white,
)
...
複製代碼
接下來,咱們在 store/ 目錄下,新增 middleware 文件用於放置中間件,並新增 AuthorizationMiddleware 類用於登陸鑑權相關業務的處理與 action 派發:
// store/middlewares.dart
class AuthorizationMiddleware extends MiddlewareClass<AppState> {
void validateUserInfo(UserModel userModel, NextDispatcher next) {
Map<String, String> errorMessage = new Map<String, String>();
if (userModel.name.isEmpty) {
errorMessage['nameErrorMessage'] = '姓名不能爲空';
}
if (userModel.email.length < 10) {
errorMessage['emailErrorMessage'] = '郵箱格式不正確';
}
if (userModel.age.toString().isNotEmpty && int.parse(userModel.age) < 0) {
errorMessage['ageErrorMessage'] = '年齡不能爲負數';
}
if (errorMessage.isNotEmpty) {
next(
new LoginFormFieldError(
errorMessage['nameErrorMessage'],
errorMessage['emailErrorMessage'],
errorMessage['ageErrorMessage'],
)
);
} else {
next(new SetLoginFlag(loginFlag: true));
}
}
@override
void call(Store<AppState> store, dynamic action, NextDispatcher next) {
if (action is ValidateUserLoginFields) {
validateUserInfo(action.userModel, next);
}
}
}
複製代碼
AuthorizationMiddleware 類,繼承了 MiddlewareClass, 咱們重寫他的 call 方法,並在其中去作 action 動做的過濾,當發送動做爲 ValidateUserLoginFields 時,調用 validateUserInfo 方法對入參進行校驗。咱們將對應的 action 傳遞到 Next 函數中,發送給下一個中間件。
在 store/middlewares 下,管理相關的中間件:
List<Middleware<AppState>> createMiddlewares() {
return [
AuthorizationMiddleware()
];
}
複製代碼
在 main.dart 中初始化中間件:
final store = Store<AppState>(
appReducer,
middleware: createMiddlewares(),
initialState: AppState(
globalState: GlobalState.initState(),
userState: UserState.initState(),
)
);
複製代碼
前面咱們提到了中間件經過 next 函數完成由 中間件 -> reducer -> 中間件 這個調度過程的,回頭看看 AuthorizationMiddleware 的方法你會發現當 action 動做並不是是 ValidateUserLoginFields 時,AuthorizationMiddleware 並無將 action 繼續向後傳遞交給下一個中間件。這便致使了整個調度過程的中止,修改 call 方法:
....
@override
void call(Store<AppState> store, dynamic action, NextDispatcher next) {
if (action is ValidateUserLoginFields) {
validateUserInfo(action.userModel, next);
}
next(action)
}
複製代碼
能夠看到這時的運行效果:
接下來,修改 AuthorizationMiddleware 中間件處理異步問題:
/// 模擬 service 異步請求
void fetchUserLogin(Store<AppState> store, UserModel userModel) async {
UserModel model = await Future.delayed(
Duration(milliseconds: 2000),
() {
return new UserModel(name: '服務返回name', age: 20, email: 'luma@qq.com');
}
);
if (model != null) {
store.dispatch(new SetUserInfo(model));
store.dispatch(new SetLoginFlag(loginFlag: true));
}
}
class AuthorizationMiddleware extends MiddlewareClass<AppState> {
void validateUserInfo(Store<AppState> store, UserModel userModel, NextDispatcher next) {
if (userModel.name.isEmpty) {
...
} else {
fetchUserLogin(store, userModel);
}
}
...
}
複製代碼
當請求校驗經過後,便在 middleware 中調用 fetchUserLogin 方法請求後臺 api 接口,根據返回值處理用戶信息了。
此時運行效果以下:
能夠看到,再點擊提交按鈕,等待2s以後,便登陸成功並拿到後臺返回的信息了。把全部的業務放到中間件來作也不是惟一的選擇,有時候咱們可能會在 viewModel 去處理各種校驗或業務,咱們的 action 可能會包含一些反作用。如何處理帶反作用的 action 呢?咱們能夠藉助 redux_thunk 這個組件來處理異步action
首先在 pubspec.yaml 引入 redux_thunk
修改 store/action.dart,新增一個異步 action:
class ReduxThunkLoginFetch {
static ThunkAction<AppState> fetchUserLogin(UserModel userModel) {
return (Store<AppState> store) async {
UserModel model = await Future.delayed(
Duration(milliseconds: 2000),
() {
return new UserModel(name: '服務返回name', age: 20, email: 'luma@qq.com');
}
);
if (model != null) {
store.dispatch(new SetUserInfo(model));
store.dispatch(new SetLoginFlag(loginFlag: true));
}
};
}
}
複製代碼
能夠看到,咱們在一個 ReduxThunkLoginFetch action 類中,增長了一個靜態方法,該方法處理了與以前 AuthorizationMiddleware 中同樣的方法,所不一樣的是,這個方法被標識爲一個 ThunkAction , 由於他內部返回了 Future.
此時在 redux_trggier 中,即可以經過調用 ReduxThunkLoginFetch.fetchUserLogin 來獲取返回:
/// redux_trigger viewModel
_fetchLoginWithThunk(UserModel userModel) {
// todo 校驗
store.dispatch(ReduxThunkLoginFetch.fetchUserLogin(userModel));
}
複製代碼
redux-thunk 中間件爲咱們攔截了 ThunkAction 類型的 action 動做。當派發動做是一個 ThunkAction 的時候,redux-thunk 會執行這個 action, 並傳遞 store 和響應的參數到 action 方法中完成異步 action 的調用。
combineReducers 是一個高階函數,能夠幫咱們組合多個 reducer 函數。 並提供了對 reducer action 的一些校驗,實際場景中可根據須要使用。
使用 redux 在對 state 狀態進行設計的時候,每每咱們但願的是全局只有一個 state 實例。就拿上面的示例來講,appState、userState、globalState 他們應該都是全局惟一,不可改變的。在 Dart 中,咱們能夠經過對類添加裝飾器的模式,來標識咱們的類是 immutable 的:
@immutable
class CountState {
final bool loginFlag;
GlobalState({
@required this.loginFlag
});
GlobalState.initState(): loginFlag = false;
}
複製代碼
dart 語法會自動檢測被裝飾的類,是否具備可變屬性(是否有 final 聲明)。
有關 immutable 能夠查看 immutable 介紹
當聲明一個類是 immutable 以後,即可在編譯階段對類的屬性進行檢測,並可防止其餘人對 state 進行修改和混入了。