Flutter MVP 封装

本文探讨了在Flutter中使用MVP模式进行开发的优势,解释了MVC与MVP的区别,指出MVP能提高代码可读性和可维护性。文章详细介绍了在Flutter中实现MVP模式的封装,包括Model、Presenter和View的封装方法,以及如何解决可能出现的问题,如内存泄漏。作者还分享了登录功能模块的MVP实践案例,展示了如何在实践中优雅地应用MVP模式。

在 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则是他们的纽带。
可能存在的问题

Model进行异步操作,获取结果通过Presenter回传到View时,出现View引用的空指针异常
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 {
///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 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供子类实现。
具体代码见最后
使用示例
这里我们以登录功能模块为例:

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 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 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 _formKey = GlobalKey();

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.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: [
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(
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);
}

}
https://www.zhihu.com/people/jia-mi-gou-po-jie
https://www.zhihu.com/people/jia-mi-gou-fu-zhi

复制代码这里的Login就是登录功能模块的view,继承BaseWidget,传入view和presenter泛型。
实现LoginContract.View接口,重写接口定义好的UI方法。
在createPresenter方法中创建LoginPresenter对象并返回。这样就可以使用getPresenter直接操作逻辑了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值