從進行開發OpenGit_Flutter項目以來,在項目中選擇哪一種架構困擾了好久。近段時間,分別在項目中嘗試了BloC
、Redux
這兩種架構,經過開發中遇到的問題,已經找到了合適的方案。爲了演示方便,我選擇了該項目的登陸流程來爲你們作演示,下面對登陸流程作下拆解。git
最終的演示效果以下所示 github
登陸界面的佈局代碼,不作過多的介紹,若是須要了解更多,能夠查看相關源碼,地址會在本文的最後貼出。redux
flutter_architecture根目錄是一個Flutter Package
,其下面分別建立了bloc
、mvc
、mvp
、redux
四個工程,lib
目錄分別是四個工程的公用模塊,例如網絡請求、日誌打印、toast提示、主頁信息展現等。以下圖所示 設計模式
該架構是在寫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
。mvc
文本監聽須要監聽帳號和密碼輸入框的輸入狀態,需聲明兩個TextEditingController
對象,相關代碼以下所示app
final TextEditingController _nameController = new TextEditingController();
final TextEditingController _passwordController = new TextEditingController();
複製代碼
在initState
處理輸入框的監聽事件,當輸入狀態改變時,並刷新頁面,更新登陸按鈕狀態,相關代碼以下所示框架
@override
void initState() {
super.initState();
_nameController.addListener(() {
setState(() {});
});
_passwordController.addListener(() {
setState(() {});
});
}
複製代碼
登陸按鈕狀態判斷須要經過帳號和密碼輸入字符串長短來判斷,當長度都大於0的時候,按鈕才能點擊,邏輯層相關代碼以下所示less
_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,
),
),
),
),
複製代碼
首先建立單例對象,並初始化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
層主要進行登陸、獲取用戶資料的網絡請求,並保存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整個框架的登陸流程已進行完成。
該架構是在進行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
作定義封裝。
觸發網絡請求,須要知足展現和隱藏loading界面,因此View
的對外須要提供這兩個最基本的接口,以下面代碼所示
abstract class IBaseView {
showLoading();
hideLoading();
}
複製代碼
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
基類中,須要提供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,能夠參考[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
基類的封裝,基類須要只須要知足登陸狀態,以下面代碼所示
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
實例中,完成整個登陸過程,咱們須要監聽帳號、密碼的輸入狀態,密碼的是否可見狀態,以及登陸狀態,以下面代碼所示
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
是網頁開發着普遍使用的設計模式,好比用在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
初始化工做。
完成登陸須要有請求登陸、請求加載中、請求錯誤、請求成功等幾個狀態,以下面代碼所示
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,以下面代碼所示
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,以下面代碼所示
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),
]);
複製代碼
登陸的中間件暫時只對其作個簡單的初始化過程,以下面代碼所示
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狀態管理方案