Flutter 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<V extends IView> {///Set or attach the view to this mPresentervoid attachView(V view);///Will be called if the view has been destroyed . Typically this method will be invoked fromvoid 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;@overridevoid attachView(IView view) {this._model = createModel();this._view = view;}@overridevoid 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;@overridevoid initState() {super.initState();presenter = createPresenter();if (presenter != null) {presenter.attachView(this);}}P createPresenter();P getPresenter() {return presenter;}@overridevoid 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 {@overridevoid dispose() {HttpManager().cancel(tag);}@overridevoid 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 {@overrideModel createModel() {return LoginModel();}@overridevoid 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);@overrideBaseWidgetState 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;@overridevoid initState() {super.initState();setTitle('');_loginNotifier = LoginNotifier();isChange = StringUtil.isBoolTrue(widget.arguments);}@overridevoid dispose() {super.dispose();_loginNotifier.dispose();}@overrideWidget 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;});}}}@overridevoid queryData() {disabledLoading();}@overridePresenter createPresenter() {return LoginPresenter();}@overridevoid 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库地址
插件地址
学习资料
- Flutter Github
- Flutter 中文网
- Flutter Packages
- Flutter 电子书
- Flutter 社区中文资源网