Flutter MVP 封装

17,024 阅读9分钟

  在 Android 开发中经常会用到一些架构,从 MVC 到 MVVP、MVVM等,这些架构会大大的解耦我们代码的功能模块,让我们的代码在项目中后期更容易扩展和维护。

  在Flutter中同样有 MVC、MVP、MVVM等架构。在Android实际开发中,也有把项目从 MVC切换到 MVP,形成了一套 MVP 快速开发框架,且做了一个 AS 快速代码生成插件。所以在 Flutter 开发中也想着是不是可以用 MVP 架构去开发,且做个一样的代码生成插件。

  所以在这是里主要看一下在 Flutter 中如何使用 MVP 模式来开发应用。

MVC

  提到MVP就不得不提到MVC,关于MVC架构,可以看下面这张图:

  MVC即Model View Controller,简单来说就是通过controller的控制去操作model层的数据,并且返回给view层展示,具体见上图。当用户出发事件的时候,view层会发送指令到controller层,接着controller去通知model层更新数据,model层更新完数据以后直接显示在view层上,这就是MVC的工作原理。

  这种原理就会造成一个致命的缺陷:当很多业务逻辑写在vidget中时,widget既充当了View层,又充当了Controller层。因此,耦合性极高,各种业务逻辑代码和View代码混合在一起,你中有我我中有你,如果要修改一个需求,改动的地方可能相当多,维护起来十分不便。

MVP

  MVP模式相当于在MVC模式中加了一个Presenter用于处理模型和逻辑,将View和Model完全独立开来,在flutter开发中的体现就是widget仅用于显示界面和交互,widget不参与模型结构和逻辑。

  使用MVP模式会使得代码多出一些接口,但是使得代码逻辑更加清晰,尤其是在处理复杂界面和逻辑时,可以对同一个widget将每一个业务都抽离成一个Presenter,这样代码既清晰逻辑明确又方便扩展。当然如果业务逻辑本身就比较简单的话使用MVP模式就显得没那么必要了。所以不需要为了用它而用它,具体的还是要根据业务需要。

  简而言之:view就是UI,model就是数据处理,而persenter则是他们的纽带。

可能存在的问题

  1. Model进行异步操作,获取结果通过Presenter回传到View时,出现View引用的空指针异常
  2. Presenter和View互相持有引用,解除不及时造成的内存泄漏。

因此,在进行MVP架构设计时需要考虑Presenter对View进行回传时,View是否为空?

Presenter与View何时解除引用即Presenter能否和View层进行生命周期同步?

  好了,说了这么多,我个人比较推荐mvp,主要是因为其相对比较简单且易上手。下面我们来看看具体如何优雅的实现MVP的封装。

MVP封装

代码结构

具体代码见最后

代码讲解

Model 封装

/// @desc  基础 model
/// @time 2019-04-22 10:33 am
/// @author Cheney
abstract class IModel {
  ///释放网络请求
  void dispose();
}


import 'package:flutter_mvp/model/i_model.dart';

/// @desc  基础 Model 生成 Tag
/// @time 2019-04-22 12:06 am
/// @author Cheney
abstract class AbstractModel implements IModel {
  String _tag;

  String get tag => _tag;

  AbstractModel() {
    _tag = '${DateTime.now().millisecondsSinceEpoch}';
  }
}

IModel 接口有一个抽象的dispose,主要用于释放网络请求。

AbstractModel抽象类实现 IModel 接口,且构造方法中生成唯一的tag 用于取消网络请求。

具体代码见最后

Present 封装

import 'package:flutter_mvp/view/i_view.dart';

/// @desc  基础 Presenter
/// @time 2019-04-22 10:30 am
/// @author Cheney
abstract class IPresenter<V extends IView> {
  ///Set or attach the view to this mPresenter
  void attachView(V view);

  ///Will be called if 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';

/// @desc  基础 Presenter,关联 View\Model
/// @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
  void detachView() {
    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();
}

IPresenter接口中设置了一泛型V继承IView,V是与presenter相关的view,且有两个抽象方法attachView,detachView。

AbstractPresenter抽象类中设置了一泛型 V继承 IView,一泛型 M继承 IModel,实现了 IPresenter,该类中持有一个View的引用,一个 Model 的引用。在 attachView绑定了 View,且生成一个 创建Model对象的抽象方法供子类实现,detachView中销毁 View、Model,这样就解决了上面说到的相互持有引用,造成内存泄漏问题。

具体代码见最后

View封装

/// @desc  基础 View
/// @time 2019-04-22 10:29 am
/// @author Cheney
abstract class IView {
  ///开始加载
  void startLoading();

  ///加载成功
  void showLoadSuccess();

  ///加载失败
  void showLoadFailure(String code, String message);

  ///无数据
  void showEmptyData({String emptyImage, String emptyText});

  ///带参数的对话框
  void startSubmit({String message});

  ///隐藏对话框
  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  基础 widget,关联 Presenter,且与生命周期关联
/// @time 2019-04-22 11:08 am
/// @author Cheney
abstract class AbstractView extends StatefulWidget {}

abstract class AbstractViewState<P extends IPresenter, V extends AbstractView>
    extends State<V> implements IView {
  P presenter;

  @override
  void initState() {
    super.initState();
    presenter = createPresenter();
    if (presenter != null) {
      presenter.attachView(this);
    }
  }

  P createPresenter();

  P getPresenter() {
    return presenter;
  }

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

IView 接口中定义了一些公共操作(加载状态、无数据状态、错误态、提交状态、统一提示等)的方法,这里大家可以根据实际的需要是否需要定义这些公共方法,我这里是默认是基类中处理,大家可参考Flutter 基类BaseWidget封装

AbstractView抽象类继承StatefulWidget,AbstractViewState中定义一泛型P继承 IPresenter,一泛型 V 继承AbstractView,实现 IView,该抽象类中持有一个 Presenter 引用,且包括两个生命周期方法initState、dispose用于创建、销毁Presenter,并调用Presenter的attachView、detachView方法关联 View、Model,并提供抽象createPresenter供子类实现。

具体代码见最后

使用示例

这里我们以登录功能模块为例:

Contract类

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';

/// @desc 登录
/// @time 2020/3/18 4:56 PM
/// @author Cheney
abstract class View implements IView {
  ///登录成功
  void loginSuccess(LoginBean loginBean);
}

abstract class Presenter implements IPresenter {
  ///登录
  void login(String phoneNo, String password);
}

abstract class Model implements IModel {
  ///登录
  void login(
      String phoneNo,
      String password,
      SuccessCallback<LoginBean> successCallback,
      FailureCallback failureCallback);
}

这里定义了登录页面的view接口、model接口和presenter 接口。

在view中,只定义与UI展示的相关方法,如登录成功等。

model负责数据请求,所以在接口中只定义了登录的方法。

presenter也只定义了登录的方法。

Model类

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';

/// @desc 登录
/// @time 2020/3/18 4:56 PM
/// @author Cheney
class LoginModel extends AbstractModel implements Model {
  @override
  void dispose() {
    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,
    );
  }
}

这里创建Model实现类,重写login方法将登录接口返回结果交给回调、重写dispose方法取消网络请求。

Presenter 类

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';

/// @desc 登录
/// @time 2020/3/18 4:56 PM
/// @author Cheney
class LoginPresenter extends AbstractPresenter<View, Model>
    implements Presenter {
  @override
  Model createModel() {
    return LoginModel();
  }

  @override
  void login(String phoneNo, String password) {
    view?.startSubmit(message: '正在登录');
    model.login(phoneNo, password, (LoginBean loginBean) {
      //取消提交框
      view?.showSubmitSuccess();
      //登录成功
      view?.loginSuccess(loginBean);
    }, (HttpError error) {
      //取消提交框、显示错误提示
      view?.showSubmitFailure(error.code, error.message);
    });
  }
}

LoginPresenter继承AbstractPresenter,传入了View和Model 泛型

实现了createModel方法创建了LoginMoel对象,实现了 login 方法,调用了 model 中的 login 方法,在回调中得到数据,也可以再进行一些逻辑判断,将结果交给view的对应的方法。

注意这里使用view?.用于解决view 为空时指针问题。

Widget类

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';

/// @desc 登录
/// @time 2020/3/18 4:56 PM
/// @author Cheney
class Login extends BaseWidget {
  ///路由
  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) {
    return ChangeNotifierProvider<LoginNotifier>.value(
      value: _loginNotifier,
      child: Container(
        color: LcfarmColor.colorFFFFFF,
        child: ListView(
          children: [
            Padding(
              padding: EdgeInsets.only(
                top: LcfarmSize.dp(24.0),
                left: LcfarmSize.dp(32.0),
              ),
              child: Text(
                '密码登录',
                style: LcfarmStyle.style80000000_32
                    .copyWith(fontWeight: FontWeight.w700),
              ),
            ),
            _formSection(),
            Padding(
              padding: EdgeInsets.only(top: LcfarmSize.dp(8.0)),
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: [
                  GestureDetector(
                    child: Padding(
                      padding: EdgeInsets.all(LcfarmSize.dp(8.0)),
                      child: Text(
                        '忘记密码',
                        style: LcfarmStyle.style3776E9_14,
                      ),
                    ),
                    behavior: HitTestBehavior.opaque,
                    onTap: () {
                      UmengConst.event(eventId: UmengConst.MMDL_WJMM);
                      NavigatorManager()
                          .pushNamed(context, Router.forgetPassword);
                    }, //点击
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }

  //表单
  Widget _formSection() {
    return Padding(
      padding: EdgeInsets.only(
          left: LcfarmSize.dp(32.0),
          top: LcfarmSize.dp(20.0),
          right: LcfarmSize.dp(32.0)),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            LcfarmSimpleInput(
              hint: '',
              label: '手机号码',
              callback: (val) {
                _phoneNo = val;
                _buttonState();
              },
              keyboardType: TextInputType.phone,
              maxLength: 11,
              /*validator: (val) {
                return val.length < 11 ? '手机号码长度错误' : null;
              },*/
            ),
            LcfarmInput(
              hint: '',
              label: '登录密码',
              callback: (val) {
                _password = val;
                _buttonState();
              },
            ),
            Consumer<LoginNotifier>(
                builder: (context, LoginNotifier loginNotifier, _) {
              return Padding(
                padding: EdgeInsets.only(top: LcfarmSize.dp(48.0)),
                child: LcfarmLargeButton(
                  label: '登录',
                  onPressed:
                      loginNotifier.isButtonDisabled ? null : _forSubmitted,
                ),
              );
            }),
          ],
        ),
      ),
    );
  }

  //输入校验
  bool _fieldsValidate() {
    //bool hasError = false;
    if (_phoneNo.length < 11) {
      return true;
    }
    if (_password.isEmpty) {
      return true;
    }
    return false;
  }

  //按钮状态更新
  void _buttonState() {
    bool hasError = _fieldsValidate();
    //状态有变化
    if (_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() {
    return LoginPresenter();
  }

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

这里的Login就是登录功能模块的view,继承BaseWidget,传入view和presenter泛型。 实现LoginContract.View接口,重写接口定义好的UI方法。

在createPresenter方法中创建LoginPresenter对象并返回。这样就可以使用getPresenter直接操作逻辑了。

代码插件

使用 MVP 会额外增加一些接口、类,且它们的格式比较统一,为了统一规范代码,相关 MVP 的代码使用AS插件来统一生成。

在 IDE中集成插件

下载插件下方插件,打开 IDE 首选项,找到 plugins , 选择install plugin from disk,找到我们刚下载的插件,重启 IDE 生效。

生成代码

在新建的 contract 类中快捷 Generate... 找到 FlutterMvpGenerator,就会生成对应模块的 model、presenter、widget 类。

最后

使用 MVP 模式,将使得应用更加好维护,同时也可以方便我们进行测试。

如果在使用过程遇到问题,欢迎下方留言交流。

Pub库地址

插件地址

学习资料

请大家不吝点赞!因为您的点赞是对我最大的鼓励,谢谢!