個人部落格
前言
從進行開發OpenGit_Flutter專案以來,在專案中選擇哪種架構困擾了很久。近段時間,分別在專案中嘗試了BloC
、Redux
這兩種架構,通過開發中遇到的問題,已經找到了合適的方案。為了演示方便,我選擇了該專案的登入流程來為大家做演示,下面對登入流程做下拆解。
- 登入首先需要輸入賬號和密碼,只有在賬號和密碼都有輸入的時候,底部登入按鈕才能點選,所以需要監聽賬號和密碼輸入框的輸入狀態,用來控制登入按鈕的點選狀態;
- 賬號輸入框需要支援一鍵刪除的功能;
- 密碼輸入框需要支援對密碼可見的功能;
- 點選登入按鈕觸發登入邏輯,在登入過程中需要展示loading介面,當登入失敗後,取消loading介面,並進行toast提示;當登入成功之後,跳轉的主介面,展示使用者的基本資訊;
- 使用者資料和token等資訊的儲存,在本文中不會提到,如需檢視該部分程式碼,點選OpenGit_Flutter;
最終的演示效果如下所示
登入介面的佈局程式碼,不做過多的介紹,如果需要了解更多,可以檢視相關原始碼,地址會在本文的最後貼出。
工程結構
flutter_architecture根目錄是一個Flutter Package
,其下面分別建立了bloc
、mvc
、mvp
、redux
四個工程,lib
目錄分別是四個工程的公用模組,例如網路請求、日誌列印、toast提示、主頁資訊展示等。如下圖所示
MVC
該架構是在寫flutter_architecture例子時最後加上的,因為在進行Android
開發的過程中,經常用它來與MVP
做對比。
架構檢視
程式入口
main.dart
是程式的入口,完成登入介面的啟動,相關程式碼如下所示
void main() => runApp(MVCApp());
class MVCApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.black,
),
home: LoginPage(),
);
}
}
複製程式碼
登入流程
由於登入狀態涉及到介面相關控制元件的重新整理,所以繼承的是StatefulWidget
。
文字監聽
文字監聽需要監聽賬號和密碼輸入框的輸入狀態,需宣告兩個TextEditingController
物件,相關程式碼如下所示
final TextEditingController _nameController = new TextEditingController();
final TextEditingController _passwordController = new TextEditingController();
複製程式碼
在initState
處理輸入框的監聽事件,當輸入狀態改變時,並重新整理頁面,更新登入按鈕狀態,相關程式碼如下所示
@override
void initState() {
super.initState();
_nameController.addListener(() {
setState(() {});
});
_passwordController.addListener(() {
setState(() {});
});
}
複製程式碼
登入按鈕狀態判斷需要通過賬號和密碼輸入字串長短來判斷,當長度都大於0的時候,按鈕才能點選,邏輯層相關程式碼如下所示
_isValidLogin() {
String name = _nameController.text;
String password = _passwordController.text;
return name.length > 0 && password.length > 0;
}
複製程式碼
登入按鈕UI層程式碼如下所示
Align _buildLoginButton(BuildContext context) {
return Align(
child: SizedBox(
height: 45.0,
width: 270.0,
child: RaisedButton(
child: Text(
'登入',
style: Theme.of(context).primaryTextTheme.headline,
),
color: Colors.black,
onPressed: _isValidLogin()
? () {
_login();
}
: null,
shape: StadiumBorder(side: BorderSide()),
),
),
);
}
複製程式碼
清空賬號輸入框
清空輸入框只需呼叫TextEditingController
clear方法,如下面程式碼所示
TextFormField _buildNameTextField() {
return new TextFormField(
controller: _nameController,
decoration: new InputDecoration(
labelText: 'Github賬號:',
suffixIcon: new GestureDetector(
onTap: () {
_nameController.clear();
},
child: new Icon(_nameController.text.length > 0 ? Icons.clear : null),
),
),
maxLines: 1,
);
}
複製程式碼
密碼是否可見
密碼是否可見主要是通過更新變數_obscureText
實現,點選事件處理邏輯很簡單,只是對_obscureText
做下取反操作,並重新整理頁面,程式碼如下所示
TextFormField _buildPasswordTextField(BuildContext context) {
return new TextFormField(
controller: _passwordController,
decoration: new InputDecoration(
labelText: 'Github密碼:',
suffixIcon: new GestureDetector(
onTap: () {
setState(() {
_obscureText = !_obscureText;
});
},
child:
new Icon(_obscureText ? Icons.visibility_off : Icons.visibility),
),
),
maxLines: 1,
obscureText: _obscureText,
);
}
複製程式碼
觸發登入
View
層點選登入按鈕,觸發Control
層登入邏輯,在Control
層通過state控制loading介面的展示和隱藏,而loading的最終狀態是由Model
層的loading狀態決定,loading UI相關程式碼如下所示:
Offstage(
offstage: !Con.isLoading,
child: new Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.black54,
child: new Center(
child: SpinKitCircle(
color: Theme.of(context).primaryColor,
size: 25.0,
),
),
),
),
複製程式碼
定義Control層
首先建立單例物件,並初始化Model
層資料,向View
層提供登入、載入狀態、使用者資料等狀態的介面,相關程式碼如下所示
class Con {
factory Con() => _getInstance();
static Con get instance => _getInstance();
static Con _instance;
Con._internal();
static Con _getInstance() {
if (_instance == null) {
_instance = new Con._internal();
}
return _instance;
}
static final model = Model();
static bool get isLoading => model.isLoading;
static UserBean get userBean => model.userBean;
Future login(State state, String name, String password) async {
state.setState(() {
_showLoading();
});
await model.login(name, password);
state.setState(() {
_hideLoading();
});
}
void _showLoading() {
model.showLoading();
}
void _hideLoading() {
model.hideLoading();
}
}
複製程式碼
定義Model層
Model
層主要進行登入、獲取使用者資料的網路請求,並儲存loading狀態以及使用者資料,相關程式碼如下所示
class Model {
bool get isLoading => _isLoading;
bool _isLoading = false;
UserBean get userBean => _userBean;
UserBean _userBean;
Future login(String name, String password) async {
final login = await LoginManager.instance.login(name, password);
//授權成功
if (login != null) {
final user = await LoginManager.instance.getMyUserInfo();
_userBean = user;
}
return;
}
void showLoading() {
_isLoading = true;
}
void hideLoading() {
_isLoading = false;
}
}
複製程式碼
網路層的相關程式碼就不再貼出,感興趣的可以在本文末尾下載原始碼進行檢視。
由上面程式碼可知,當View
層觸發登入時,呼叫了Control
層login
介面,在該介面內,實現了展示loading狀態,並等待登入的網路請求,當請求完成後,則取消loading狀態,最終交給View
層進行資料處理,相關處理的程式碼如下所示
_login() async {
String name = _nameController.text;
String password = _passwordController.text;
await Con.instance.login(this, name, password);
if (Con.userBean != null) {
NavigatorUtil.goHome(context, Con.userBean);
} else {
ToastUtil.showToast('登入失敗,請重新登入');
}
}
複製程式碼
到此,MVC整個框架的登入流程已進行完成。
MVP
該架構是在進行Android開發時,是一種比較常用的架構。
架構檢視
程式入口
main.dart
是程式的入口,完成登入介面的啟動,相關程式碼如下所示
void main() => runApp(MVPApp());
class MVPApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.black,
),
home: LoginPage(),
);
}
}
複製程式碼
登入流程
與MVC
一致,可以參考MVC
。
文字監聽
與MVC
一致,可以參考MVC
。
清空賬號輸入框
與MVC
一致,可以參考MVC
。
密碼是否可見
與MVC
一致,可以參考MVC
。
觸發登入
View
層點選登入按鈕,觸發Presenter
層登入邏輯,在Presenter
層通過View
層提供的介面來控制loading介面的展示和隱藏,loading UI相關程式碼如下所示
Offstage(
offstage: !isLoading,
child: new Container(
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height,
color: Colors.black54,
child: new Center(
child: SpinKitCircle(
color: Theme.of(context).primaryColor,
size: 25.0,
),
),
),
),
複製程式碼
上面程式碼中的isLoading
狀態已在基類中做了統一封裝,下面對MVP
做定義封裝。
封裝View層
觸發網路請求,需要滿足展示和隱藏loading介面,所以View
的對外需要提供這兩個最基本的介面,如下面程式碼所示
abstract class IBaseView {
showLoading();
hideLoading();
}
複製程式碼
封裝Presenter層
Presenter
層的公共介面只需提供對View
層的註冊以及反註冊,如下面程式碼所示
abstract class IBasePresenter<V extends IBaseView> {
void onAttachView(V view);
void onDetachView();
}
複製程式碼
下面對Presenter
層的程式碼實現上面所提供的介面,如下面程式碼所示
abstract class BasePresenter<V extends IBaseView> extends IBasePresenter<V> {
V view;
@override
void onAttachView(IBaseView view) {
this.view = view;
}
@override
void onDetachView() {
this.view = null;
}
}
複製程式碼
封裝State基類
在State
基類中,需要提供Presenter
的初始化的方法、loading狀態、資料初始化以及檢視的構建等,如下面程式碼所示
abstract class BaseState<T extends StatefulWidget, P extends BasePresenter<V>,
V extends IBaseView> extends State<T> implements IBaseView {
P presenter;
bool isLoading = false;
P initPresenter();
Widget buildBody(BuildContext context);
void initData() {
}
@override
void initState() {
super.initState();
presenter = initPresenter();
if (presenter != null) {
presenter.onAttachView(this);
}
initData();
}
@override
void dispose() {
super.dispose();
if (presenter != null) {
presenter.onDetachView();
presenter = null;
}
}
@override
@mustCallSuper
Widget build(BuildContext context) {
return new Scaffold(
body: buildBody(context),
);
}
@override
void showLoading() {
setState(() {
isLoading = true;
});
}
@override
void hideLoading() {
setState(() {
isLoading = false;
});
}
}
複製程式碼
到此,MVP
框架已經封裝完,下面只需對登入介面做相應的實現即可。
實現登入邏輯
在進行登入時,Presenter
層需要向View
層提供登入介面,當進行登入完畢後,需要向View
層進行登入狀態的反饋,所以View
需要提供登入成功、失敗兩個介面,如下面程式碼所示
abstract class ILoginPresenter<V extends ILoginView> extends BasePresenter<V> {
void login(String name, String password);
}
abstract class ILoginView extends IBaseView {
void onLoginSuccess(UserBean userBean);
void onLoginFailed();
}
複製程式碼
當相關介面定義完畢後,首先實現登入的Presenter
層的程式碼,如下面程式碼所示
class LoginPresenter extends ILoginPresenter {
@override
void login(String name, String password) async {
if (view != null) {
view.showLoading();
}
final login = await LoginManager.instance.login(name, password);
//授權成功
if (login != null) {
final user = await LoginManager.instance.getMyUserInfo();
if (user != null) {
if (view != null) {
view.hideLoading();
view.onLoginSuccess(user);
} else {
view.hideLoading();
view.onLoginFailed();
}
}
} else {
if (view != null) {
view.hideLoading();
view.onLoginFailed();
}
}
}
}
複製程式碼
然後對登入State
的程式碼進行實現,如下面程式碼所示
class _LoginPageState extends BaseState<LoginPage, LoginPresenter, ILoginView>
implements ILoginView {
@override
void initData() {
super.initData();
}
@override
Widget buildBody(BuildContext context) {
return null;
}
@override
LoginPresenter initPresenter() {
return LoginPresenter();
}
@override
void onLoginSuccess(UserBean userBean) {
NavigatorUtil.goHome(context, userBean);
}
@override
void onLoginFailed() {
ToastUtil.showToast('登入失敗,請重新登入');
}
}
複製程式碼
相關程式碼已經封裝完畢,最後只需呼叫登入相關邏輯,如下面程式碼所示
_login() {
if (presenter != null) {
String name = _nameController.text;
String password = _passwordController.text;
presenter.login(name, password);
}
}
複製程式碼
BloC
關於什麼是BloC,可以參考[Flutter Package]狀態管理之BLoC的封裝和Flutter | 狀態管理探索篇——BLoC(三)。
架構檢視
程式入口
main.dart
是程式的入口,完成登入介面的啟動,相關程式碼如下所示
void main() => runApp(BlocApp());
class BlocApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.black,
),
home: BlocProvider<LoginBloc>(
child: LoginPage(),
bloc: LoginBloc(),
),
);
}
}
複製程式碼
上面程式碼跟MVC
和MVP
有不同之處,傳入home
的物件是BlocProvider
,且其包含了child
和bloc
例項。如下面程式碼所示
class BlocProvider<T extends BaseBloc> extends StatefulWidget {
final T bloc;
final Widget child;
BlocProvider({
Key key,
@required this.child,
@required this.bloc,
}) : super(key: key);
@override
_BlocProviderState<T> createState() {
return _BlocProviderState<T>();
}
static T of<T extends BaseBloc>(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<BaseBloc>> {
static final String TAG = "_BlocProviderState";
@override
void initState() {
super.initState();
LogUtil.v('initState ' + T.toString(), tag: TAG);
}
@override
Widget build(BuildContext context) {
LogUtil.v('build ' + T.toString(), tag: TAG);
return widget.child;
}
@override
void dispose() {
super.dispose();
LogUtil.v('dispose ' + T.toString(), tag: TAG);
widget.bloc.dispose();
}
}
複製程式碼
登入流程
BLoC能夠允許我們分離業務邏輯,不用考慮什麼時候需要重新整理螢幕,一切交給StreamBuilder和BLoC就可以完成,所以登入頁面繼承StatelessWidget
即可。如下面程式碼所示
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder(
stream: bloc.stream,
initialData: initialData(),
builder: (BuildContext context,
AsyncSnapshot<LoadingBean<LoginBlocBean>> snapshot) {
}
);
}
}
複製程式碼
stream
代表了這個stream builder監聽的流,這裡監聽的是LoginBloc
的stream;initData
代表初始的值,因為在首次渲染的時候,還未與使用者產生互動,也就不會有事件從流中流出,所以需要給首次渲染一個初始值;builder
函式接收一個位置引數BuildContext和一個snapshot,snapshot就是這個流輸出的資料的一個快照,我們可以通過snapshot.data訪問快照中的資料,StreamBuilder中的builder是一個AsyncWidgetBuilder,它能夠非同步構建widget,當檢測到有資料從流中流出時,將會重新構建。
建立BloC
首先完成BloC
基類的封裝,基類需要只需要滿足登入狀態,如下面程式碼所示
class LoadingBean<T> {
bool isLoading;
T data;
LoadingBean({this.isLoading, this.data});
@override
String toString() {
return 'LoadingBean{isLoading: $isLoading, data: $data}';
}
}
abstract class BaseBloc<T extends LoadingBean> {
static final String TAG = "BaseBloc";
BehaviorSubject<T> _subject = BehaviorSubject<T>();
Sink<T> get sink => _subject.sink;
Stream<T> get stream => _subject.stream;
void dispose() {
_subject.close();
sink.close();
}
}
複製程式碼
建立BloC例項
在登入的BloC
例項中,完成整個登入過程,我們需要監聽賬號、密碼的輸入狀態,密碼的是否可見狀態,以及登入狀態,如下面程式碼所示
class LoginBloc extends BaseBloc<LoadingBean<LoginBlocBean>> {
LoadingBean<LoginBlocBean> bean;
LoginBloc() {
bean = LoadingBean<LoginBlocBean>(
isLoading: false,
data: LoginBlocBean(
name: '',
password: '',
obscure: true,
),
);
}
changeObscure() {
}
changeName(String name) {
}
changePassword(String password) {
}
login(BuildContext context) async {
}
void _showLoading() {
bean.isLoading = true;
sink.add(bean);
}
void _hideLoading() {
bean.isLoading = false;
sink.add(bean);
}
}
複製程式碼
文字監聽
建立賬號和密碼兩個TextEditingController
例項,並完成其事件監聽,如下面程式碼所示
final TextEditingController _nameController = new TextEditingController();
final TextEditingController _passwordController = new TextEditingController();
LoginBloc bloc = BlocProvider.of<LoginBloc>(context);
_nameController.addListener(() {
bloc.changeName(_nameController.text);
});
_passwordController.addListener(() {
bloc.changePassword(_passwordController.text);
});
複製程式碼
當文字發生改變時,會呼叫LoginBloc
裡相應的改變方法,並對相應的文字進行重新複雜,在通過sink.add()
更新介面,如下面程式碼所示
changeName(String name) {
bean.data.name = name;
sink.add(bean);
}
changePassword(String password) {
bean.data.password = password;
sink.add(bean);
}
複製程式碼
清空賬號輸入框
與MVC
一致,可以參考MVC
。
密碼是否可見
需要改變可見狀態,呼叫LoginBloc
中的changeObscure
方法,如下面程式碼所示
changeObscure() {
bean.data.obscure = !bean.data.obscure;
sink.add(bean);
}
複製程式碼
觸發登入
需要進行網路請求,控制loading的展示和隱藏,這裡需要呼叫LoginBloc
中的login
方法,當登入成功後,則跳轉主頁展示基本資訊,不成功則toast提示,如下面程式碼所示
login(BuildContext context) async {
_showLoading();
final login =
await LoginManager.instance.login(bean.data.name, bean.data.password);
//授權成功
if (login != null) {
final user = await LoginManager.instance.getMyUserInfo();
if (user != null) {
NavigatorUtil.goHome(context, user);
} else {
ToastUtil.showToast('登入失敗,請重新登入');
}
} else {
ToastUtil.showToast('登入失敗,請重新登入');
}
_hideLoading();
}
複製程式碼
Redux
Redux
是網頁開發著廣泛使用的設計模式,比如用在React.js中。關於它的介紹可以參考文章Flutter主題切換之flutter redux。
架構檢視
程式入口
main.dart
是程式的入口,完成登入介面的啟動,相關程式碼如下所示
void main() {
final store = new Store<AppState>(
appReducer,
initialState: AppState.initial(),
middleware: [
LoginMiddleware(),
],
);
runApp(
ReduxApp(
store: store,
),
);
}
class ReduxApp extends StatelessWidget {
final Store<AppState> store;
const ReduxApp({Key key, this.store}) : super(key: key);
@override
Widget build(BuildContext context) {
return StoreProvider<AppState>(
store: store,
child: StoreConnector<AppState, _ViewModel>(
converter: _ViewModel.fromStore,
builder: (context, vm) {
return MaterialApp(
theme: ThemeData(
primaryColor: Colors.black,
),
home: LoginPage(),
);
},
),
);
}
}
class _ViewModel {
_ViewModel();
static _ViewModel fromStore(Store<AppState> store) {
return _ViewModel();
}
}
複製程式碼
在程式的入口處,對Store
進行了初始化工作,完成了對reducer
、state
、middleware
初始化工作。
定義action
完成登入需要有請求登入、請求載入中、請求錯誤、請求成功等幾個狀態,如下面程式碼所示
class FetchLoginAction {
final BuildContext context;
final String userName;
final String password;
FetchLoginAction(this.context, this.userName, this.password);
}
class ReceivedLoginAction {
ReceivedLoginAction(
this.token,
this.userBean,
);
final String token;
final UserBean userBean;
}
class RequestingLoginAction {}
class ErrorLoadingLoginAction {}
複製程式碼
初始化state
目前只有一個登入功能,所以只需一個登入的state,如下面程式碼所示
class AppState {
final LoginState loginState;
AppState({
this.loginState,
});
factory AppState.initial() => AppState(
loginState: LoginState.initial(),
);
}
class LoginState {
final bool isLoading;
final String token;
LoginState({this.isLoading, this.token});
factory LoginState.initial() {
return LoginState(
isLoading: false,
token: '',
);
}
LoginState copyWith({bool isLoading, String token}) {
return LoginState(
isLoading: isLoading ?? this.isLoading,
token: token ?? this.token,
);
}
}
複製程式碼
初始化reducer
目前只有一個登入功能,所以只需一個登入的reducer,如下面程式碼所示
AppState appReducer(AppState state, action) {
return AppState(
loginState: loginReducer(state.loginState, action),
);
}
final loginReducer = combineReducers<LoginState>([
TypedReducer<LoginState, RequestingLoginAction>(_requestingLogin),
TypedReducer<LoginState, ReceivedLoginAction>(_receivedLogin),
TypedReducer<LoginState, ErrorLoadingLoginAction>(_errorLoadingLogin),
]);
複製程式碼
初始化middleware
登入的中介軟體暫時只對其做個簡單的初始化過程,如下面程式碼所示
class LoginMiddleware extends MiddlewareClass<AppState> {
static final String TAG = "LoginMiddleware";
@override
void call(Store store, action, NextDispatcher next) {
}
}
複製程式碼
登入流程
Redux能夠允許我們分離業務邏輯,不用考慮什麼時候需要重新整理螢幕,一切交給StoreConnector可以完成,所以登入頁面繼承StatelessWidget
即可。如下面程式碼所示
class LoginPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, LoginPageViewModel>(
distinct: true,
converter: (store) => LoginPageViewModel.fromStore(store, context),
builder: (_, viewModel) => LoginPageContent(viewModel),
);
}
}
複製程式碼
LoginPageViewModel
只負責登入狀態以及登入行為,如下面程式碼所示
typedef OnLogin = void Function(String name, String password);
class LoginPageViewModel {
static final String TAG = "LoginPageViewModel";
final OnLogin onLogin;
final bool isLoading;
LoginPageViewModel({this.onLogin, this.isLoading});
static LoginPageViewModel fromStore(
Store<AppState> store, BuildContext context) {
return LoginPageViewModel(
isLoading: store.state.loginState.isLoading,
onLogin: (String name, String password) {
LogUtil.v('name is $name, password is $password', tag: TAG);
store.dispatch(FetchLoginAction(context, name, password));
},
);
}
}
複製程式碼
文字監聽
與MVC
一致,可以參考MVC
。
清空賬號輸入框
與MVC
一致,可以參考MVC
。
密碼是否可見
與MVC
一致,可以參考MVC
。
觸發登入
在進行登入時,我們只需呼叫LoginPageViewModel
內的onLogin
方法,該方法會通過store
分發出FetchLoginAction
,此時中介軟體LoginMiddleware
會收到該行為,並對其進行處理。
@override
void call(Store store, action, NextDispatcher next) {
next(action);
if (action is FetchLoginAction) {
_doLogin(next, action.context, action.userName, action.password);
}
}
複製程式碼
上面對收到的行為繼續分發出去,如果是自己感興趣的行為,就自己進行操作,處理FetchLoginAction
行為,相關程式碼如下所示
Future<void> _doLogin(NextDispatcher next, BuildContext context,
String userName, String password) async {
next(RequestingLoginAction());
try {
LoginBean loginBean =
await LoginManager.instance.login(userName, password);
if (loginBean != null) {
String token = loginBean.token;
LoginManager.instance.setToken(loginBean.token, true);
UserBean userBean = await LoginManager.instance.getMyUserInfo();
if (userBean != null) {
next(ReceivedLoginAction(token, userBean));
NavigatorUtil.goHome(context, userBean);
} else {
ToastUtil.showToast('登入失敗請重新登入');
LoginManager.instance.setToken(null, true);
}
} else {
ToastUtil.showToast('登入失敗請重新登入');
next(ErrorLoadingLoginAction());
}
} catch (e) {
LogUtil.v(e, tag: TAG);
ToastUtil.showToast('登入失敗請重新登入');
next(ErrorLoadingLoginAction());
}
}
複製程式碼
在進行登入的過程中,最初會發出正在請求的行為RequestingLoginAction
,當登入成功後也會發出行為ReceivedLoginAction
,登入失敗後發出行為ErrorLoadingLoginAction
,而這些發出的行為都會被reducer
收到,並對資料進行處理,在通知UI重新整理。loginReducer
相關處理邏輯如下面程式碼所示
LoginState _requestingLogin(LoginState state, action) {
LogUtil.v('_requestingLogin', tag: TAG);
return state.copyWith(isLoading: true);
}
LoginState _receivedLogin(LoginState state, action) {
LogUtil.v('_receivedLogin', tag: TAG);
return state.copyWith(isLoading: false, token: action.token);
}
LoginState _errorLoadingLogin(LoginState state, action) {
LogUtil.v('_errorLoadingLogin', tag: TAG);
return state.copyWith(isLoading: false);
}
複製程式碼
總結
區域性狀態和全域性狀態
上面的登入例子中,登入表單的任何驗證型別,都可以考慮為區域性狀態,因為這些規則僅適用於這個元件,而App的其他部分不需要知道這個型別。但是從後臺獲取的token和使用者資料,就需要考慮成全域性狀態,因為它影響整個app的作用域(未登入和已登陸),而且可能別的元件會依賴它。
選擇
對比上面四種架構的好壞,最終還是的迴歸到狀態管理上來。MVC
、MVP
的狀態管理都是採用setState
方式,而BloC
和Redux
都有自己的一套狀態管理。
當專案最初不是很複雜的時候,採用setState
方式更新資料是可以的。但是隨著功能的增加,你的專案將會有幾十個甚至上百個狀態,setState
出現的次數便會顯著增加,每次setState
都會重新呼叫build方法,這勢必對於效能以及程式碼的可閱讀性帶來一定的影響。所以就放棄了MVC
、MVP
這兩種架構。
最初對OpenGit_Flutter進行架構重構的時候,用到的是Redux
,到涉及到多個頁面複用時,例如專案中的專案頁
,每涉及到一個複用頁面就需要在state
內定義一些列的變數,這是個很痛苦的過程,所以後面就放棄了用Redux
,但是Redux
在儲存全域性狀態有優勢,例如主題、語言、使用者資料等。後面又嘗試了BloC
,該架構在多頁面複用時,就沒存在Redux
的問題。
所以最後我採用的架構是Bloc+Redux
,用BloC
控制區域性狀態,用Redux
控制全域性狀態。同時大家也可以參考文章[譯]讓我來幫你理解和選擇Flutter狀態管理方案
專案地址
- 架構Sample:flutter_architecture
- OpenGit_Flutter專案:OpenGit_Flutter
- OpenGit_Flutter專案BloC嘗試:OpenGit_Flutter
- OpenGit_Flutter專案Redux嘗試:OpenGit_Flutter