Personal blog

preface

Since the development of the OpenGit_Flutter project, it has been difficult to decide which architecture to choose in the project. Recently, the two architectures BloC and Redux have been tried in the project respectively, and the appropriate scheme has been found through the problems encountered in the development. In order to demonstrate convenience, I chose the login process of the project to demonstrate for you. The login process is disassembled below.

  1. Login first need to enter the account and password, only when the account and password are entered, the login button at the bottom can be clicked, so need to monitor the account and password input box input state, used to control the click state of the login button;
  2. The account input box must support the one-click deletion function.
  3. The password input box must support the function of visible password.
  4. Click the login button to trigger the login logic. The loading interface needs to be displayed during the login. If the login fails, cancel the loading interface and propose a toast message. After a successful login, the main screen displays basic user information.
  5. The saving of user profiles and tokens is not mentioned in this article. To view this code, click on OpenGit_Flutter.

The final demo looks like this

Login interface layout code, not too much introduction, if you need to know more, you can view the source code, the address will be posted at the end of this article.

Engineering structure

The flutter_architecture root directory is a Flutter Package, under which four projects bloc, MVC, MVP and Redux are created respectively. The lib directory is the common module of the four projects, such as network request, log printing, toast prompt, home page information display, etc. As shown in the figure below

MVC

This architecture was added last when I wrote the Flutter_Architecture example because it is often compared to MVP during Android development.

Architectural view

Program entrance

Dart is the entry point of the program, and the login interface is started. The relevant code is shown below

void main() => runApp(MVCApp());

class MVCApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnMaterialApp( theme: ThemeData( primaryColor: Colors.black, ), home: LoginPage(), ); }}Copy the code

The login process

Because the login state involves the refresh of the interface related controls, the StatefulWidget inherits.

The text to monitor

Text listeners need to listen for the input state of the account and password fields by declaring two TextEditingController objects, as shown below

final TextEditingController _nameController = new TextEditingController();
final TextEditingController _passwordController = new TextEditingController();
Copy the code

InitState handles the listening event of the input box, and when the input state changes, it refreshes the page and updates the login button state, as shown below

@override
void initState() {
    super.initState();

    _nameController.addListener(() {
      setState(() {});
    });
    _passwordController.addListener(() {
      setState(() {});
    });
}
Copy the code

The status of the login button is determined by the length of the input string of the account and password. The button can be clicked only when the length of the input string is greater than 0

_isValidLogin() {
    String name = _nameController.text;
    String password = _passwordController.text;

    return name.length > 0 && password.length > 0;
}
Copy the code

The login button UI layer code is shown below

Align _buildLoginButton(BuildContext context) {
    return Align(
      child: SizedBox(
        height: 45.0,
        width: 270.0,
        child: RaisedButton(
          child: Text(
            'login',
            style: Theme.of(context).primaryTextTheme.headline,
          ),
          color: Colors.black,
          onPressed: _isValidLogin()
              ? () {
                  _login();
                }
              : null,
          shape: StadiumBorder(side: BorderSide()),
        ),
      ),
    );
}
Copy the code

Clear the account input box

To clear the input field, simply call the TextEditingController Clear method, as shown in the code below

  TextFormField _buildNameTextField() {
    return new TextFormField(
      controller: _nameController,
      decoration: new InputDecoration(
        labelText: 'making account:,
        suffixIcon: new GestureDetector(
          onTap: () {
            _nameController.clear();
          },
          child: new Icon(_nameController.text.length > 0 ? Icons.clear : null),
        ),
      ),
      maxLines: 1,); }Copy the code

Password visible

Whether the password is visible is mainly realized by updating variable _obscureText. The click event processing logic is very simple, just reverse operation of _obscureText and refresh the page, the code is shown as follows

TextFormField _buildPasswordTextField(BuildContext context) {
    return new TextFormField(
      controller: _passwordController,
      decoration: new InputDecoration(
        labelText: 'making password:,
        suffixIcon: newGestureDetector( onTap: () { setState(() { _obscureText = ! _obscureText; }); }, child:new Icon(_obscureText ? Icons.visibility_off : Icons.visibility),
        ),
      ),
      maxLines: 1,
      obscureText: _obscureText,
    );
}
Copy the code

Trigger the login

Click the login button in the View layer to trigger the login logic in the Control layer. State controls the display and hiding of loading interface in the Control layer, and the final state of loading is determined by the loading state in the Model layer. The relevant codes of loading UI are shown as follows:

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(), (), (), (Copy the code
Define the Control layer

Firstly, the singleton object is created, and the Model layer data is initialized to provide the interface of login, loading state, user information and other states to the View layer. The relevant code is shown as follows

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(); }}Copy the code
Define the Model layer

The Model layer mainly performs network requests for login, obtaining user data, and saving the loading state and user data. The relevant codes are shown as follows

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);
    // Authorization succeeded
    if(login ! =null) {
      final user = await LoginManager.instance.getMyUserInfo();
      _userBean = user;
    }
    return;
  }

  void showLoading() {
    _isLoading = true;
  }

  void hideLoading() {
    _isLoading = false; }}Copy the code

The network layer code is no longer posted, but you can download the source code at the end of this article.

As can be seen from the above code, when the View layer triggers login, the login interface of the Control layer is invoked. In this interface, the loading state is displayed and the network request for login is waited. When the request is completed, the loading state is cancelled and the data is finally handed over to the View layer for processing

_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('Login failed, please log in again'); }}Copy the code

At this point, the MVC framework login process is complete.

MVP

This architecture is a common architecture in Android development.

Architectural view

Program entrance

Dart is the entry point of the program, and the login interface is started. The relevant code is shown below

void main() => runApp(MVPApp());

class MVPApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnMaterialApp( theme: ThemeData( primaryColor: Colors.black, ), home: LoginPage(), ); }}Copy the code

The login process

Consistent with MVC, you can refer to MVC.

The text to monitor

Consistent with MVC, you can refer to MVC.

Clear the account input box

Consistent with MVC, you can refer to MVC.

Password visible

Consistent with MVC, you can refer to MVC.

Trigger the login

Click the login button in the View layer to trigger the login logic in the Presenter layer. In the Presenter layer, the loading interface can be displayed and hidden through the interface provided by the View layer. The relevant codes of loading UI are shown as follows

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(), (), (), (Copy the code

The isLoading state in the code above has been uniformly encapsulated in the base class. The MVP definition is encapsulated in the following.

Encapsulate the View layer

To trigger a network request, you need to show and hide the loading interface, so the View needs to provide the two basic interfaces, as shown in the following code

abstract class IBaseView {
  showLoading();

  hideLoading();
}
Copy the code
Encapsulation Presenter layer

The public interface for the Presenter layer only provides registration and unregistration to the View layer, as shown in the code below

abstract class IBasePresenter<V extends IBaseView> {
  void onAttachView(V view);

  void onDetachView();
}
Copy the code

The code below implements the interface provided above for the Presenter layer, as shown below

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; }}Copy the code
Encapsulate the State base class

In the State base class, you need to provide initialization methods for Presenter, loading State, data initialization, and view construction, as shown in the code below

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; }); }}Copy the code

At this point, the MVP framework has been packaged, the login interface to do the corresponding implementation.

Implementing logon logic

When logging in, the Presenter layer needs to provide a login interface to the View layer. After logging in, the View layer needs to provide feedback on the login status. Therefore, the View layer needs to provide two interfaces: successful and failed login, as shown in the following code

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();
}
Copy the code

When the relevant interfaces are defined, first implement the code for the Presenter layer of the login, as shown below

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);
    // Authorization succeeded
    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(); }}}}Copy the code

The code to log in to State is then implemented, as shown below

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 failed, please log in again'); }}Copy the code

The relevant code has been wrapped, and you only need to invoke the logon-related logic, as shown in the following code

 _login() {
    if(presenter ! =null) {
      String name = _nameController.text;
      Stringpassword = _passwordController.text; presenter.login(name, password); }}Copy the code

BloC

About what is a BloC, can refer to [Flutter Package] the BloC of state management of encapsulation and Flutter | state management to explore – BloC (3).

Architectural view

Program entrance

Dart is the entry point of the program, and the login interface is started. The relevant code is shown below

void main() => runApp(BlocApp());

class BlocApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnMaterialApp( theme: ThemeData( primaryColor: Colors.black, ), home: BlocProvider<LoginBloc>( child: LoginPage(), bloc: LoginBloc(), ), ); }}Copy the code

The above code differs from MVC and MVP in that the object passed to home is BlocProvider, which contains instances of Child and Bloc. As shown in the code below

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(); }}Copy the code

The login process

BLoC allows us to separate the business logic without worrying about when we need to refresh the screen, leaving it all to The StreamBuilder and BLoC, so the login page inherits the StatelessWidget. As shown in the code below

class LoginPage extends StatelessWidget {
    @override
    Widget build(BuildContext context) {
        returnStreamBuilder( stream: bloc.stream, initialData: initialData(), builder: (BuildContext context, AsyncSnapshot<LoadingBean<LoginBlocBean>> snapshot) { } ); }}Copy the code
  • streamRepresents the stream that the Stream Builder is listening for, in this caseLoginBlocThe stream;
  • initDataRepresents the initial value, because at the time of the first rendering, there is no interaction with the user, so no events will flow out of the stream, so the first rendering needs an initial value;
  • builderThe function takes a location argument, BuildContext, and a snapshot. Snapshot is a snapshot of the output data of this stream. We can access the data in the snapshot through snapshot.data. The Builder in StreamBuilder is an AsyncWidgetBuilder that can asynchronously build widgets that will be rebuilt when data is detected coming out of the stream.

Create a BloC

First, the BloC base class is encapsulated. The base class only needs to meet the login status, as shown in the code below

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;

  voiddispose() { _subject.close(); sink.close(); }}Copy the code

Create BloC instance

In the logged BloC instance, to complete the whole login process, we need to monitor the input status of the account and password, whether the password is visible, and the login status, as shown in the code below

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); }}Copy the code

The text to monitor

Create two instances of TextEditingController, account and password, and complete their event listening, as shown in the code below

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);
});
Copy the code

When the text is changed, the corresponding change method in LoginBloc will be called, and the corresponding text will be re-complicated in the update interface through sink.add(), as shown in the code below

changeName(String name) {
    bean.data.name = name;
    sink.add(bean);
}

changePassword(String password) {
    bean.data.password = password;
    sink.add(bean);
}
Copy the code

Clear the account input box

Consistent with MVC, you can refer to MVC.

Password visible

To change the visible state, call the changeObscure method in LoginBloc, as shown in the code below

changeObscure() { bean.data.obscure = ! bean.data.obscure; sink.add(bean); }Copy the code

Trigger the login

Network request is required to control the display and hiding of loading. Login method in LoginBloc needs to be called here. If the login is successful, the main page will be switched to show basic information; if the login is unsuccessful, toast will be given, as shown in the code below

login(BuildContext context) async {
    _showLoading();

    final login =
        await LoginManager.instance.login(bean.data.name, bean.data.password);
    // Authorization succeeded
    if(login ! =null) {
      final user = await LoginManager.instance.getMyUserInfo();
      if(user ! =null) {
        NavigatorUtil.goHome(context, user);
      } else {
        ToastUtil.showToast('Login failed, please log in again'); }}else {
      ToastUtil.showToast('Login failed, please log in again');
    }

    _hideLoading();
}
Copy the code

Redux

Redux is a widely used design pattern for web development, such as in react.js. An introduction to this topic can be found in the article “Flutter Redux”, which switches to the topic of Flutter.

Architectural view

Program entrance

Dart is the entry point of the program, and the login interface is started. The relevant code is shown below

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) {
          returnMaterialApp( theme: ThemeData( primaryColor: Colors.black, ), home: LoginPage(), ); },),); }}class _ViewModel {
  _ViewModel();

  static _ViewModel fromStore(Store<AppState> store) {
    return_ViewModel(); }}Copy the code

At the entrance of the program, Store was initialized, and reducer, state and Middleware were initialized.

Define the action

To complete the login, there are several states, such as request login, request loading, request error, request success, as shown in the following code

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 {}
Copy the code

Initialize the state

Currently there is only one login function, so only one login state is required, as shown in the code below

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, ); }}Copy the code

Initialize the reducer

Currently there is only one login function, so there is only one Reducer for the login, as shown in the code below

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),
]);
Copy the code

Initialize the middleware

The logged-in middleware does a simple initialization for now, as shown in the code below

class LoginMiddleware extends MiddlewareClass<AppState> {
  static final String TAG = "LoginMiddleware";

  @override
  void call(Store store, action, NextDispatcher next) {
  }
}
Copy the code

The login process

Redux allows us to separate the business logic without worrying about when we need to refresh the screen, leaving it to StoreConnector so that the login page inherits the StatelessWidget. As shown in the code below

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), ); }}Copy the code

LoginPageViewModel is only responsible for login status and login behavior, as shown in the following code

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)); }); }}Copy the code

The text to monitor

Consistent with MVC, you can refer to MVC.

Clear the account input box

Consistent with MVC, you can refer to MVC.

Password visible

Consistent with MVC, you can refer to MVC.

Trigger the login

To log in, we simply call the onLogin method inside LoginPageViewModel, which distributes the FetchLoginAction through the Store, at which point the middleware LoginMiddleware receives the action and processes it.

@override
void call(Store store, action, NextDispatcher next) {
    next(action);
    if (action isFetchLoginAction) { _doLogin(next, action.context, action.userName, action.password); }}Copy the code

If it is an action that you are interested in, you can proceed to handle the FetchLoginAction, as shown in the code below

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('Login failed please login again');
          LoginManager.instance.setToken(null.true); }}else {
        ToastUtil.showToast('Login failed please login again'); next(ErrorLoadingLoginAction()); }}catch (e) {
      LogUtil.v(e, tag: TAG);
      ToastUtil.showToast('Login failed please login again'); next(ErrorLoadingLoginAction()); }}Copy the code

During login, the action that is being requested is initially issued as the RequestingLoginAction, the action ReceivedLoginAction is also issued when the login is successful, and the action ErrorLoadingLoginAction is issued when the login fails. These sent behaviors will be received by reducer, and the data will be processed and updated in the notification UI. The loginReducer processing logic is shown in the following code

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);
}
Copy the code

conclusion

Local state and global state

In the login example above, any validation type of the login form can be considered a local state because the rules only apply to this component and the rest of the App does not need to know about the type. However, tokens and user data obtained from the background need to be considered global because it affects the scope of the entire app (unlogged and logged in) and other components may depend on it.

choose

Comparing the above four architectures, it comes back to state management. The state management of MVC and MVP adopts the setState approach, while BloC and Redux have their own state management.

It is ok to update data using setState when the project is not very complex initially. However, as functionality increases, your project will have dozens or even hundreds of states, and the number of setStates will increase significantly. Each time setState is called again, the build method will have an impact on performance and code readability. So we abandoned the MVC and MVP architectures.

Redux was used in the initial architectural reconstruction of OpenGit_Flutter. When multiple page reuse was involved, such as the project page of a project, a list of variables needed to be defined in state for each page reuse. This was a painful process, so we gave up using Redux later. But Redux has the advantage of saving global state, such as topics, languages, user profiles, and so on. BloC was later tried, and there was no Redux problem in the multi-page reuse of this architecture.

So the architecture I finally adopted was Bloc+Redux, with Bloc controlling the local state and Redux controlling the global state. Let me help you understand and choose a Flutter state management solution

The project address

  • Architecture Sample: Flutter_architecture
  • OpenGit_Flutter project: OpenGit_Flutter
  • OpenGit_Flutter project BloC Attempt: OpenGit_Flutter
  • OpenGit_Flutter project Redux attempts: OpenGit_Flutter