MVC、MVP、BloC、Redux四種架構在Flutter上的嘗試

我的博客

前言

從進行開發OpenGit_Flutter項目以來,在項目中選擇哪一種架構困擾了好久。近段時間,分別在項目中嘗試了BloCRedux這兩種架構,經過開發中遇到的問題,已經找到了合適的方案。爲了演示方便,我選擇了該項目的登陸流程來爲你們作演示,下面對登陸流程作下拆解。git

  1. 登陸首先須要輸入帳號和密碼,只有在帳號和密碼都有輸入的時候,底部登陸按鈕才能點擊,因此須要監聽帳號和密碼輸入框的輸入狀態,用來控制登陸按鈕的點擊狀態;
  2. 帳號輸入框須要支持一鍵刪除的功能;
  3. 密碼輸入框須要支持對密碼可見的功能;
  4. 點擊登陸按鈕觸發登陸邏輯,在登陸過程當中須要展現loading界面,當登陸失敗後,取消loading界面,並進行toast提示;當登陸成功以後,跳轉的主界面,展現用戶的基本信息;
  5. 用戶資料和token等信息的保存,在本文中不會提到,如需查看該部分代碼,點擊OpenGit_Flutter

最終的演示效果以下所示 github

登陸界面的佈局代碼,不作過多的介紹,若是須要了解更多,能夠查看相關源碼,地址會在本文的最後貼出。redux

工程結構

flutter_architecture根目錄是一個Flutter Package,其下面分別建立了blocmvcmvpredux四個工程,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(),
    );
  }
}
複製代碼

登陸流程

因爲登陸狀態涉及到界面相關控件的刷新,因此繼承的是StatefulWidgetmvc

文本監聽

文本監聽須要監聽帳號和密碼輸入框的輸入狀態,需聲明兩個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,
                ),
            ),
    ),
),
複製代碼
定義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層觸發登陸時,調用了Controllogin接口,在該接口內,實現了展現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(),
      ),
    );
  }
}
複製代碼

上面代碼跟MVCMVP有不一樣之處,傳入home的對象是BlocProvider,且其包含了childbloc實例。以下面代碼所示

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進行了初始化工做,完成了對reducerstatemiddleware初始化工做。

定義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的做用域(未登陸和已登錄),並且可能別的組件會依賴它。

選擇

對比上面四種架構的好壞,最終仍是的迴歸到狀態管理上來。MVCMVP的狀態管理都是採用setState方式,而BloCRedux都有本身的一套狀態管理。

當項目最初不是很複雜的時候,採用setState方式更新數據是能夠的。可是隨着功能的增長,你的項目將會有幾十個甚至上百個狀態,setState出現的次數便會顯著增長,每次setState都會從新調用build方法,這勢必對於性能以及代碼的可閱讀性帶來必定的影響。因此就放棄了MVCMVP這兩種架構。

最初對OpenGit_Flutter進行架構重構的時候,用到的是Redux,到涉及到多個頁面複用時,例如項目中的項目頁,每涉及到一個複用頁面就須要在state內定義一些列的變量,這是個很痛苦的過程,因此後面就放棄了用Redux,可是Redux在保存全局狀態有優點,例如主題、語言、用戶資料等。後面又嘗試了BloC,該架構在多頁面複用時,就沒存在Redux的問題。

因此最後我採用的架構是Bloc+Redux,用BloC控制局部狀態,用Redux控制全局狀態。同時你們也能夠參考文章[譯]讓我來幫你理解和選擇Flutter狀態管理方案

項目地址

相關文章
相關標籤/搜索