likes
comments
collection
share

Flutter MVP 封装

作者站长头像
站长
· 阅读数 44

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

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

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

MVC

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

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

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

MVP

Flutter 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封装

代码结构

Flutter 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;
    }
  }
}

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

具体代码见最后

使用示例

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

Flutter MVP 封装

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 生效。

Flutter MVP 封装

生成代码

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

Flutter MVP 封装

最后

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

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

Pub库地址

插件地址

学习资料

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

转载自:https://juejin.cn/post/6844904104469233671
评论
请登录