MVC、MVP、BloC、Redux四种架构在Flutter上的尝试

个人博客

前言

从进行开发OpenGit_Flutter项目以来,在项目中选择哪种架构困扰了很久。近段时间,分别在项目中尝试了BloCRedux这两种架构,通过开发中遇到的问题,已经找到了合适的方案。为了演示方便,我选择了该项目的登录流程来为大家做演示,下面对登录流程做下拆解。

  1. 登录首先需要输入账号和密码,只有在账号和密码都有输入的时候,底部登录按钮才能点击,所以需要监听账号和密码输入框的输入状态,用来控制登录按钮的点击状态;
  2. 账号输入框需要支持一键删除的功能;
  3. 密码输入框需要支持对密码可见的功能;
  4. 点击登录按钮触发登录逻辑,在登录过程中需要展示loading界面,当登录失败后,取消loading界面,并进行toast提示;当登录成功之后,跳转的主界面,展示用户的基本信息;
  5. 用户资料和token等信息的保存,在本文中不会提到,如需查看该部分代码,点击OpenGit_Flutter

最终的演示效果如下所示


image

image

登录界面的布局代码,不做过多的介绍,如果需要了解更多,可以查看相关源码,地址会在本文的最后贴出。

工程结构

flutter_architecture根目录是一个Flutter Package,其下面分别创建了blocmvcmvpredux四个工程,lib目录分别是四个工程的公用模块,例如网络请求、日志打印、toast提示、主页信息展示等。如下图所示

image

MVC

该架构是在写flutter_architecture例子时最后加上的,因为在进行Android开发的过程中,经常用它来与MVP做对比。

架构视图

image

程序入口

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层触发登录时,调用了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开发时,是一种比较常用的架构。

架构视图

image

程序入口

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(三)

架构视图

image

程序入口

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

架构视图

flutter_architecture_redux.png

程序入口

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状态管理方案

项目地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345