Flutter MVP 封装

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

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

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

MVC

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

image

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

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

MVP

image

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

代码结构

image

具体代码见最后

代码讲解

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 接口中定义了一些公共操作(加载状态、无数据状态、错误态、提交状态、统一提示等)的方法,这里大家可以根据实际的需要是否需要定义这些公共方法。

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

具体代码见最后

使用示例

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

image

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 模式,将使得应用更加好维护,同时也可以方便我们进行测试。

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

Pub库地址

学习资料

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

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

推荐阅读更多精彩内容