Some architectures are often used in Android development, ranging from MVC to MVVP and MVVM. These architectures will greatly decouples the functional modules of our code, making it easier to expand and maintain our code in the middle and later stages of the project.

Flutter also has MVC, MVP, MVVM and other architectures. In the actual development of Android, I also switched the project from MVC to MVP, formed a set of MVP rapid development framework, and made an AS fast code generation plug-in. So in Flutter development, I wondered if I could use the MVP architecture and make a similar code generation plug-in.

So here is a look at how to use MVP mode to develop applications for Flutter.

MVC

MVP can’t be mentioned without MVC. For MVC architecture, see the following diagram:

This principle leads to a fatal flaw: When much of the business logic is written in Vidget, the widget acts as both the View layer and the Controller layer. Therefore, the coupling is very high, all kinds of business logic code and View code mixed together, you have me and I have you, if you want to modify a requirement, the changes may be quite a lot, very inconvenient to maintain.

MVP

Using MVP mode makes the code have more interfaces, but makes the code logic clearer. Especially when dealing with complex interfaces and logic, you can separate each business from the same widget into a Presenter, so that the code is clear and logical and easy to expand. Of course, using the MVP model is not necessary if the business logic itself is simple. So you don’t need to use it just for the sake of using it, it depends on the business needs.

In short: View is UI, Model is data processing, and Persenter is their link.

Possible problems

  1. A null pointer to a View reference exception occurs when a View reference is returned to the View by Presenter
  2. Presenter and View hold references to each other, relieving memory leaks caused by delays.

Therefore, when designing the MVP architecture, we need to consider whether the View is empty when the Presenter sends back messages to it.

When are Presenter and View dereferenced that is, can Presenter synchronize its life cycle with the View layer?

Having said that, I personally recommend the MVP, mainly because it’s relatively simple and easy to get started. Let’s take a look at how to elegantly encapsulate MVP.

MVP encapsulation

The code structure

See the code at the end

The code on

The Model encapsulates

// @time 2019-04-22 10:33am // @author Cheney abstract class IModel {/// void dispose(); } import'package:flutter_mvp/model/i_model.dart'; @time 2019-04-22 12:06am @author Cheney abstract class AbstractModel implements IModel { String _tag; String get tag => _tag;AbstractModel() {
    _tag = '${DateTime.now().millisecondsSinceEpoch}'; }}Copy the code

The IModel interface has an abstract Dispose, which is mainly used to release network requests.

The AbstractModel abstract class implements the IModel interface and generates a unique tag in the constructor to cancel the network request.

See the code at the end

The Present package

import 'package:flutter_mvp/view/i_view.dart'; @author Cheney abstract class IPresenter<V extends IView> { ///Set or attach the view to this mPresenter void attachView(V view); ///Will be calledif the view has been destroyed . Typically this method will be invoked from
  void detachView();
}


import 'package:flutter_mvp/model/i_model.dart';
import 'package:flutter_mvp/presenter/i_presenter.dart';
import 'package:flutter_mvp/view/i_view.dart'; @time 2019-04-22 10:51 am @author Cheney Abstract class AbstractPresenter<V  extends IView, M extends IModel> implements IPresenter { M _model; V _view; @override void attachView(IView view) { this._model = createModel(); this._view = view; } @override voiddetachView() {
    if(_view ! = null) { _view = null; }if(_model ! = null) { _model.dispose(); _model = null; } } V get view {return _view;
  }

//  V get view => _view;

  M get model => _model;

  IModel createModel();
}

Copy the code

The IPresenter interface has a generic V inheriting IView. V is a view associated with presenter and has two abstract methods attachView and detachView.

The AbstractPresenter abstract class contains a generic V that inherits IView and a generic M that inherits IModel, which implements IPresenter. This class holds a reference to a View and a reference to a Model. DetachView destroys the View and Model when attachView binds the View and generates an abstract method to create the Model object for subclasses to implement. This solves the memory leak problem mentioned above.

See the code at the end

The View to encapsulate

// @time 2019-04-22 10:29 am // @author Cheney abstract class IView {/// startLoading void startLoading(); Void showLoadSuccess(); Void showLoadFailure(String code, String message); Void showEmptyData({String emptyImage, String emptyText}); Void startSubmit({String message}); /// Hide dialog void showSubmitSuccess(); Void showSubmitFailure(String code, String message); Void showTips(String message); } import'package:flutter/material.dart';
import 'package:flutter_mvp/mvp/presenter/i_present.dart';
import 'package:flutter_mvp/mvp/view/i_view.dart'; /// @desc base widget, associated Presenter, // @time 2019-04-22 11:08am @author Cheney Abstract Class AbstractView extends StatefulWidget {} abstract  class AbstractViewState<P extends IPresenter, V extends AbstractView> extends State<V> implements IView { P presenter; @override voidinitState() {
    super.initState();
    presenter = createPresenter();
    if(presenter ! = null) { presenter.attachView(this); } } P createPresenter(); PgetPresenter() {
    return presenter;
  }

  @override
  void dispose() {
    super.dispose();
    if(presenter ! = null) { presenter.detachView(); presenter = null; }}}Copy the code

The IView interface defines some common operation methods (load state, no data state, error state, submit state, unified prompt, etc.). You can define these public methods according to your actual needs.

AbstractView abstract classes inherit StatefulWidget, AbstractViewState defines a generic P inherit IPresenter, and a generic V inherit AbstractView. Implement IView, an abstract class that holds a Presenter reference and contains two life-cycle methods, initState and Dispose, which create and dispose Presenter. The attachView and detachView methods of the Presenter are called to associate the View and Model with the View and provide the abstract createPresenter for subclasses to implement.

See the code at the end

Use the sample

Here we take the login function module as an example:

Contract class

import 'package:flutter_mvp/model/i_model.dart';
import 'package:flutter_mvp/presenter/i_presenter.dart';
import 'package:flutter_mvp/view/i_view.dart';
import 'package:kappa_app/base/api.dart';

import 'login_bean.dart'; // @time 2020/3/18 4:56pm // @author Cheney abstract class View implements IView {/// loginSuccess(LoginBean loginBean); } abstract class Presenter implements IPresenter {/// login(String phoneNo, String password); } abstract class Model implements IModel {/// login(String phoneNo, String password, SuccessCallback<LoginBean> successCallback, FailureCallback failureCallback); }Copy the code

The view, Model, and Presenter interfaces for the login page are defined here.

In view, only methods related to the UI presentation are defined, such as login success.

Model is responsible for data requests, so only login methods are defined in the interface.

Presenter also defines only login methods.

Model class

import 'package:flutter_common_utils/http/http_error.dart';
import 'package:flutter_common_utils/http/http_manager.dart';
import 'package:flutter_mvp/model/abstract_model.dart';
import 'package:kappa_app/base/api.dart';

import 'login_bean.dart';
import 'login_contract.dart'; @author Cheney extends AbstractModel implements Model {// @author Cheney extends AbstractModel implements Model { @override voiddispose() {
    HttpManager().cancel(tag);
  }

  @override
  void login(
      String phoneNo,
      String password,
      SuccessCallback<LoginBean> successCallback,
      FailureCallback failureCallback) {
    HttpManager().post(
      url: Api.login,
      data: {'phoneNo': phoneNo, 'password': password}, successCallback: (data) { successCallback(LoginBean.fromJson(data)); }, errorCallback: (HttpError error) { failureCallback(error); }, tag: tag, ); }}Copy the code

The Model implementation class is created, the Login method is overridden to give the result returned by the login interface to the callback, and the Dispose method is overridden to cancel the network request.

Presenter class

import 'package:flutter_common_utils/http/http_error.dart';
import 'package:flutter_mvp/presenter/abstract_presenter.dart';

import 'login_bean.dart';
import 'login_contract.dart';
import 'login_model.dart'; @author Cheney class LoginPresenter extends AbstractPresenter<View, Model> implements Presenter { @override ModelcreateModel() {
    returnLoginModel(); } @override void login(String phoneNo, String password) { view? .startSubmit(message:'Logging in'); Model.login (phoneNo, password, (LoginBean LoginBean) {// Cancel submit box view? .showSubmitSuccess(); // Successful login view? .loginSuccess(loginBean); }, (HttpError error) {// Cancel the submission box, display error message view? .showSubmitFailure(error.code, error.message); }); }}Copy the code

LoginPresenter inherits AbstractPresenter, passing in View and Model generics

The createModel method is implemented to create the LoginMoel object, the login method is implemented, the login method in the model is called, and the data is obtained in the callback. You can also make some logical judgment, and give the result to the corresponding method of the view.

Notice the use of view here? . Used to solve the pointer problem when view is empty.

The Widget class

import 'package:flutter/material.dart';
import 'package:flutter_common_utils/lcfarm_size.dart';
import 'package:kappa_app/base/base_widget.dart';
import 'package:kappa_app/base/navigator_manager.dart';
import 'package:kappa_app/base/router.dart';
import 'package:kappa_app/base/umeng_const.dart';
import 'package:kappa_app/utils/encrypt_util.dart';
import 'package:kappa_app/utils/lcfarm_color.dart';
import 'package:kappa_app/utils/lcfarm_style.dart';
import 'package:kappa_app/utils/string_util.dart';
import 'package:kappa_app/widgets/lcfarm_input.dart';
import 'package:kappa_app/widgets/lcfarm_large_button.dart';
import 'package:kappa_app/widgets/lcfarm_simple_input.dart';
import 'package:provider/provider.dart';

import 'login_bean.dart';
import 'login_contract.dart';
import 'login_notifier.dart';
import 'login_presenter.dart'; // @time 2020/3/18 4:56pm // @author Cheney class Login extends BaseWidget {/// Route static const String router ="login";

  Login({Object arguments}) : super(arguments: arguments, routerName: router);

  @override
  BaseWidgetState getState() {
    return _LoginState();
  }
}

class _LoginState extends BaseWidgetState<Presenter, Login> implements View {
  LoginNotifier _loginNotifier;
  GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  String _phoneNo = ' ';
  String _password = ' ';
  bool _submiting = false;

  bool isChange = false;

  @override
  void initState() {
    super.initState();
    setTitle(' ');
    _loginNotifier = LoginNotifier();
    isChange = StringUtil.isBoolTrue(widget.arguments);
  }

  @override
  void dispose() {
    super.dispose();
    _loginNotifier.dispose();
  }

  @override
  Widget buildWidget(BuildContext context) {
    returnChangeNotifierProvider<LoginNotifier>.value( value: _loginNotifier, child: Container( color: LcfarmColor.colorFFFFFF, child: ListView( children: [ Padding( padding: EdgeInsets.only( top: Lcfarmsie.dp (24.0), left: lcfarmsie.dp (32.0),), child: Text('Password login', style: LcfarmStyle.style80000000_32 .copyWith(fontWeight: FontWeight.w700), ), ), _formSection(), Padding( padding: (top: EdgeInsets. Only LcfarmSize. Dp (8.0)), the child: Row (mainAxisAlignment: mainAxisAlignment center, crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( child: Padding( padding: EdgeInsets. All (LcfarmSize. Dp (8.0)), the child: the Text ('Forget password', style: LcfarmStyle.style3776E9_14, ), ), behavior: HitTestBehavior.opaque, onTap: () { UmengConst.event(eventId: UmengConst.MMDL_WJMM); NavigatorManager() .pushNamed(context, Router.forgetPassword); }, // click),],),],),),),),); } // Form Widget_formSection() {
    returnPadding-right (Padding: EdgeInsets. Only (left: lcfarmsie.dp (32.0), top: LCfarmsie.dp (20.0), right: Lcfarmsie.dp (32.0)), child: Form(key: _formKey, child: Column(children: <Widget>) [LcfarmSimpleInput(hint:' ',
              label: 'Mobile number',
              callback: (val) {
                _phoneNo = val;
                _buttonState();
              },
              keyboardType: TextInputType.phone,
              maxLength: 11,
              /*validator: (val) {
                return val.length < 11 ? 'Wrong length of mobile number' : null;
              },*/
            ),
            LcfarmInput(
              hint: ' ',
              label: 'Login password',
              callback: (val) {
                _password = val;
                _buttonState();
              },
            ),
            Consumer<LoginNotifier>(
                builder: (context, LoginNotifier loginNotifier, _) {
              returnPadding(Padding: edgeinset.only (top: lcfarmsie.dp (48.0)), child: LcfarmLargeButton(label:'login', onPressed: loginNotifier.isButtonDisabled ? null : _forSubmitted, ), ); }),],),); } // Enter check bool_fieldsValidate() {
    //bool hasError = false;
    if (_phoneNo.length < 11) {
      return true;
    }
    if (_password.isEmpty) {
      return true;
    }
    return false; } // Button status updates void_buttonState() { bool hasError = _fieldsValidate(); // The state has changedif(_loginNotifier.isButtonDisabled ! = hasError) { _loginNotifier.isButtonDisabled = hasError; } } void_forSubmitted() {
    var _form = _formKey.currentState;
    if (_form.validate()) {
      //_form.save();
      if(! _submiting) { _submiting =true;
        UmengConst.event(eventId: UmengConst.MMDL_DL);
        EncryptUtil.encode(_password).then((pwd) {
          getPresenter().login(_phoneNo, pwd);
        }).catchError((e) {
          print(e);
        }).whenComplete(() {
          _submiting = false;
        });
      }
    }
  }

  @override
  void queryData() {
    disabledLoading();
  }

  @override
  Presenter createPresenter() {
    returnLoginPresenter(); } @override void loginSuccess(LoginBean loginBean) async { await SpUtil().putString(Const.token, loginBean.token); await SpUtil().putString(Const.username, _phoneNo); NavigatorManager().pop(context); }}Copy the code

Login is the View of the Login function module, inheriting BaseWidget and passing in View and Presenter generics. Implement loginContract. View interface, rewrite interface defined UI methods.

Create a LoginPresenter object in the createPresenter method and return it. This allows you to manipulate the logic directly using getPresenter.

The plug-in code

The use of MVP will add some additional interfaces, classes, and their format is relatively uniform, in order to unify the specification code, related MVP code using AS plug-in unified generation.

Integrate plug-ins into the IDE

Under the plugin, open the IDE preferences, find plugins, select Install Plugin from Disk, find the plugin we just downloaded, and restart the IDE to take effect.

The generated code

In the new contract class Generate quickly… Finding the FlutterMvpGenerator generates the model, Presenter, and Widget classes for the corresponding module.

The last

Using MVP mode will make the application easier to maintain and easier to test.

If you encounter problems in the process of using, please leave a comment below.

The Pub base address

Plug-in address

Learning materials

  • Flutter Github
  • Because Chinese website
  • Flutter Packages
  • Flutter ebook
  • Flutter Community Chinese resource website

Please give a thumbs up! Because your likes are the biggest encouragement to me, thank you!