mvp模式的优点
mvp模式将视图、业务逻辑、数据模型隔离,使用mvp模式,能使复杂的业务逻辑变得更加清晰,使代码更具有灵活性和扩展性,正是这些优点,使mvp模式广泛应用于原生开发中。
flutter使用mvp之前
以前原生开发页面,只需要花费少量的时间,就可以通过原生提供的可视化拖拽功能,迅速的完成一个简单的页面布局效果和配置,而逻辑代码只需要引用布局文件即可完成交互。然而flutter开发中目前还没有提供可视化的拖拽功能,实现页面布局和控件需要一行行码代码,因此在页面布局、元素上将会花费大量的编码时间,这对于原生开发的工程师的我来说,感觉十分不习惯。既然现在还没有提供可视化UI编辑功能,那我们也只能按照标准一行行编写UI了。flutter核心要素就是widget,所有页面元素都是widget,下面来看看使用mvp之前的代码:
import 'dart:convert'; import 'package:badge/badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_biobank/entity/IntResult.dart'; import 'package:flutter_biobank/entity/SampleResult.dart'; import 'package:flutter_biobank/entity/TextResult.dart'; import 'package:flutter_biobank/page/work/SampleCartsPage.dart'; import 'package:flutter_biobank/res/colors.dart'; import 'package:flutter_biobank/res/images.dart'; import 'package:flutter_biobank/res/urls.dart'; import 'package:flutter_biobank/util/DialogUtil.dart'; import 'package:flutter_biobank/util/HttpUtil.dart'; import 'package:flutter_biobank/util/NavigatorUtil.dart'; import 'package:flutter_biobank/widget/PageLoadView.dart'; import 'package:flutter_biobank/widget/SmartRefresh.dart'; import 'package:fluttertoast/fluttertoast.dart'; import "package:pull_to_refresh/pull_to_refresh.dart"; ///样本申领 class SampleClaimPage extends StatefulWidget { @override State<StatefulWidget> createState() { return new _SampleClaimPageState(); } } class _SampleClaimPageState extends State<SampleClaimPage> { int pageIndex = 1; //当前页码 RefreshController _controller; List<Sample> samples = new List(); //列表中的样本集合 List<Sample> checkedSamples = new List(); //选中的样本集合 int loadStatus; //当前页面加载状态 int samplesCount = 0; //申领车中样本数量 @override void initState() { super.initState(); _controller = new RefreshController(); getSamples(); getSamplesCount(); } @override Widget build(BuildContext context) { return new Scaffold( backgroundColor: page_background, appBar: new AppBar( title: new Text("样本申领"), actions: <Widget>[ Container( alignment: Alignment.center, margin: EdgeInsets.only(right: 8), child: new Badge.left( child: IconButton( icon: ImageIcon(AssetImage(Images.IMG_ICON_CARTS)), onPressed: () { NavigatorUtil.startIntent(context, new SampleCartsPage()); }, ), positionTop: 0, borderSize: 0, positionRight: 0, value: " $samplesCount "), ) ], ), body: new PageLoadView( status: loadStatus, child: _buildContent(), offstage: samples.length > 0, onTap: () { setState(() { loadStatus = PageLoadStatus.loading; getSamples(); }); }, ), ); } ///构建内容显示布局 Widget _buildContent() { return new Column( children: <Widget>[ _buildRefresh(), _buildBottom(), ], ); } ///构建底部控件 Widget _buildBottom() { return Container( color: Colors.white, child: Column( children: <Widget>[ new Divider(height: 0.5, color: devider_black), new Row( children: <Widget>[ Expanded(child: Container(child: new Text("选中样本:${checkedSamples.length}"), padding: EdgeInsets.symmetric(horizontal: 16))), GestureDetector( child: new Container( alignment: Alignment.center, color: Colors.red, width: 120, height: 60, child: new Text("加入申领", style: new TextStyle(color: Colors.white, fontSize: 16))), onTap: () { if (checkedSamples.length <= 0) { Fluttertoast.showToast(msg: "请选择样本"); } else { doJoinCarts(); } }, ), ], ), ], ), ); } ///构建刷新和加载控件 Widget _buildRefresh() { return new SmartRefresh( controller: _controller, child: _buildListView(), onRefresh: () { //下拉刷新 pageIndex = 1; return getSamples(); }, onLoadMore: (bool) { //上拉加载更多 getSamples(); }, ); } ///构建ListView Widget _buildListView() { return new ListView.builder( physics: new AlwaysScrollableScrollPhysics(), itemBuilder: _buildListViewItem, itemCount: samples.length, ); } ///构建listItem Widget _buildListViewItem(BuildContext context, int index) { return Card( child: Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 2), child: new Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Checkbox( value: samples[index].isSelected, onChanged: (bool) { setState(() { samples[index].isSelected = bool; bool ? checkedSamples.add(samples[index]) : checkedSamples.remove(samples[index]); }); }), new Expanded( child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ new Text("${samples[index].SerialNumber}", style: new TextStyle(fontSize: 18)), new Text( "样本名称:${samples[index].Name}", style: new TextStyle(color: Colors.black45), softWrap: false, overflow: TextOverflow.fade, ), new Text("可用量:${samples[index].AvailableVolume}", style: new TextStyle(color: Colors.black45)), new Text("位置:${samples[index].Location}", style: new TextStyle(color: Colors.black45)), ], )), ], ), ), ); } ///加载数据 Future getSamples() async { await HttpUtil.getInstance().post(Urls.URL_GET_SAMPLES, data: { "PageIndex": pageIndex, "PageSize": 20, "State": 1, "Sort": "asc", "BeginTime": "", "BoxCode": "", "EndTime": "", "Location": "", "Name": "", "ProjectID": null, "erialNumber": "", "StudyID": null, }, callBack: (success, data) { _controller.sendBack(false, RefreshStatus.idle); if (success) { SampleResult result = SampleResult.fromJson(json.decode(data)); //解析json if (result.code == 200) { if (pageIndex == 1) { samples.clear(); //下拉刷新时,先清除原来的数据 checkedSamples.clear(); //下拉刷新时,选中的数据也清空 } samples.addAll(result.rows); pageIndex += 1; //数据加载成功后,page+1 samples.length >= result.total ? _controller.sendBack(false, RefreshStatus.noMore) : null; } else { Fluttertoast.showToast(msg: result.message); loadStatus = PageLoadStatus.failed; } } else { loadStatus = PageLoadStatus.failed; } setState(() {}); }); } ///加入申领车 Future doJoinCarts() async { DialogUtil.showLoading(context); //显示加载对话框 List<int> ids = new List(); for (Sample sample in checkedSamples) { ids.add(sample.ID); } await HttpUtil.getInstance().post(Urls.URL_CARTS_ADD, data: json.encode(ids), callBack: (success, data) { Navigator.pop(context); if (success) { TextResult result = TextResult.fromJson(json.decode(data)); DialogUtil.showTips(context, text: result.message); pageIndex = 1; getSamples(); getSamplesCount(); } else { Fluttertoast.showToast(msg: data); } }); } ///查询申领车中样本数量 Future getSamplesCount() async { await HttpUtil.getInstance().get(Urls.URL_CARTS_SAMPLESCOUNT, callBack: (success, data) { if (success) { IntResult result = IntResult.fromJson(json.decode(data)); if (result.code == 200) { setState(() { samplesCount = result.response; }); } } }); } }
这只是一个简单的页面,调用列表查询接口,用ListView显示列表数据,调用数量查询接口,查询购物车中数量,并显示在appBar的action中。业务逻辑和页面代码混合在一起,导致代码量大,类看起来很臃肿,如果业务逻辑更加复杂的情况下,代码阅读、代码审查及功能维护都不是件容易的事。
flutter使用mvp之后
下面我们使用mvp模式对上面的代码进行改造,首先我们先将view的改变行为抽象出来,建立viewMode
abstract class IClaimPageView { void querySamplesSuccess(SampleResult result); void querySamplesFailed(); void queryCartsSampleCountSuccess(int count); void doJoinCartsSuccess(String message); void doJoinCartsFailed(String message); }
该类定义的方法分别表示列表查询成功或失败了,查询数量成功了,加入购物车成功或失败了,页面分别要做的各种事情,具体页面要做什么变化,就交给View去实现,也就是页面View,实现viewModel。
class _SampleClaimPageState extends State<SampleClaimPage> implements IClaimPageView { @override void querySamplesSuccess(SampleResult result) { //TODO } @override void querySamplesFailed() { //TODO } @override void queryCartsSampleCountSuccess(int count) { //TODO } @override void doJoinCartsFailed(String message) { //TODO } @override void doJoinCartsSuccess(String message) { //TODO } }
接下来,我们需要将业务逻辑代码分离出去,建立Presenter类,传入viewModel的引用,并定义方法实现业务逻辑。
///样本申领presenter class ClaimPresenter extends BasePresenter { ClaimModel _model; IClaimPageView _view; ClaimPresenter(this._view) { _model = new ClaimModel(); } ///分页查询库存中的样本 Future querySamples(int pageIndex) async { await _model.querySamples({ "PageIndex": pageIndex, "PageSize": 20, "State": 1, "Sort": "asc", "BeginTime": "", "BoxCode": "", "EndTime": "", "Location": "", "Name": "", "ProjectID": null, "erialNumber": "", "StudyID": null, }, (bool, result) { if (_view == null) { return; } if (bool) { _view.querySamplesSuccess(result); } else { _view.querySamplesFailed(); } }); } ///查询申领车中样本数量 Future queryCartsSampleCount() async { await _model.queryCartsSampleCount((bool, int) { if (_view == null) { return; } if (bool) { _view.queryCartsSampleCountSuccess(int); } }); } ///加入申领车 Future doJoinCarts(BuildContext context, List<Sample> samples) async { DialogUtil.showLoading(context); List<int> ids = new List(); for (Sample sample in samples) { ids.add(sample.ID); } await _model.doJoinCarts(json.encode(ids), (bool, message) { Navigator.pop(context); if (_view == null) { return; } if (bool) { _view.doJoinCartsSuccess(message); } else { _view.doJoinCartsFailed(message); } }); } @override void dispose() { _view = null; } }
这里的ClaimModel 实际上就是数据请求代码,原本数据请求也是可以写在presenter类中的,但是为了使代码更具灵活性和解耦性,我们这里将数据请求层也抽取出去,这样我其它页面也需要查询购物车中样本数量时,只需要几句简单的代码即可实现。
class ClaimModel { ///查询样本列表 Future querySamples(data, Function(bool, Object) callBack) async { await HttpUtil.getInstance().post(Urls.URL_GET_SAMPLES, data: data, callBack: (success, data) { if (success) { SampleResult result = SampleResult.fromJson(json.decode(data)); //解析json if (result.code == 200) { callBack(true, result); } else { callBack(false, result.message); } } else { callBack(false, data); } }); } ///查询申领车中的样本数量 Future queryCartsSampleCount(Function(bool, int) callBack) async { await HttpUtil.getInstance().get(Urls.URL_CARTS_SAMPLESCOUNT, callBack: (success, data) { if (success) { IntResult result = IntResult.fromJson(json.decode(data)); if (result.code == 200) { callBack(true, result.response); } } }); } ///加入申领车 Future doJoinCarts(data, Function(bool, String) callBack) async { await HttpUtil.getInstance().post(Urls.URL_CARTS_ADD, data: data, callBack: (success, data) { if (success) { TextResult result = TextResult.fromJson(json.decode(data)); callBack(true, result.message); } else { callBack(true, data); } }); } }
最后就是View的完整代码了
import 'package:badge/badge.dart'; import 'package:flutter/material.dart'; import 'package:flutter_biobank/entity/SampleResult.dart'; import 'package:flutter_biobank/page/work/SampleCartsPage.dart'; import 'package:flutter_biobank/page/work/claim/ClaimPresenter.dart'; import 'package:flutter_biobank/page/work/claim/IClaimPageView.dart'; import 'package:flutter_biobank/res/colors.dart'; import 'package:flutter_biobank/res/images.dart'; import 'package:flutter_biobank/util/DialogUtil.dart'; import 'package:flutter_biobank/util/Logger.dart'; import 'package:flutter_biobank/util/NavigatorUtil.dart'; import 'package:flutter_biobank/widget/PageLoadView.dart'; import 'package:flutter_biobank/widget/SmartRefresh.dart'; import 'package:fluttertoast/fluttertoast.dart'; import "package:pull_to_refresh/pull_to_refresh.dart"; ///样本申领 class SampleClaimPage extends StatefulWidget { @override State<StatefulWidget> createState() { return new _SampleClaimPageState(); } } class _SampleClaimPageState extends State<SampleClaimPage> implements IClaimPageView { static final String TAG = "SampleClaimPageState"; int pageIndex = 1; //当前页码 RefreshController _controller; //控制器,控制加载更多的显示状态 List<Sample> samples = new List(); //列表中的样本集合 List<Sample> checkedSamples = new List(); //选中的样本集合 int loadStatus; //当前页面加载状态 int samplesCount = 0; //申领车中样本数量 ClaimPresenter _presenter; @override void initState() { super.initState(); Logger.log(TAG, "initState"); _controller = new RefreshController(); _presenter = new ClaimPresenter(this); _presenter.querySamples(pageIndex); _presenter.queryCartsSampleCount(); } @override void dispose() { super.dispose(); if (_presenter != null) { _presenter.dispose(); _presenter = null; } } @override Widget build(BuildContext context) { return new Scaffold( backgroundColor: page_background, appBar: new AppBar( title: new Text("样本申领"), actions: <Widget>[ Container( alignment: Alignment.center, margin: EdgeInsets.only(right: 8), child: _buildBadge(context), ) ], ), body: _buildBody(), ); } ///构建购物车按钮 Widget _buildBadge(BuildContext context) { if (samplesCount > 0) { return new Badge.left( child: IconButton( icon: ImageIcon(AssetImage(Images.IMG_ICON_CARTS)), onPressed: () { NavigatorUtil.startIntent(context, new SampleCartsPage()); }, ), positionTop: 0, borderSize: 0, positionRight: 0, value: " $samplesCount "); } else { return new IconButton( icon: ImageIcon(AssetImage(Images.IMG_ICON_CARTS)), onPressed: () { NavigatorUtil.startIntent(context, new SampleCartsPage()); }, ); } } ///构建body Widget _buildBody() { return new PageLoadView( status: loadStatus, child: new Column( children: <Widget>[ _buildRefresh(), _buildBottom(), ], ), offstage: samples.length > 0, onTap: () { setState(() { loadStatus = PageLoadStatus.loading; _presenter.querySamples(pageIndex); }); }, ); } ///构建底部控件 Widget _buildBottom() { return Container( color: Colors.white, child: Column( children: <Widget>[ new Divider(height: 0.5, color: devider_black), new Row( children: <Widget>[ Expanded(child: Container(child: new Text("选中样本:${checkedSamples.length}"), padding: EdgeInsets.symmetric(horizontal: 16))), GestureDetector( child: new Container( alignment: Alignment.center, color: Colors.red, width: 120, height: 60, child: new Text("加入申领", style: new TextStyle(color: Colors.white, fontSize: 16))), onTap: () { if (checkedSamples.length <= 0) { Fluttertoast.showToast(msg: "请选择样本"); } else { _presenter.doJoinCarts(context, checkedSamples); } }, ), ], ), ], ), ); } ///构建刷新和加载控件 Widget _buildRefresh() { return new SmartRefresh( controller: _controller, child: _buildListView(), onRefresh: () { return _presenter.querySamples(pageIndex = 1); //下拉刷新 }, onLoadMore: (bool) { //上拉加载更多 _presenter.querySamples(pageIndex); }, ); } ///构建ListView Widget _buildListView() { return new ListView.builder( physics: new AlwaysScrollableScrollPhysics(), itemBuilder: _buildListViewItem, itemCount: samples.length, ); } ///构建listItem Widget _buildListViewItem(BuildContext context, int index) { return Card( child: Container( padding: EdgeInsets.symmetric(vertical: 8, horizontal: 2), child: new Row( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ new Checkbox( value: samples[index].isSelected, onChanged: (bool) { setState(() { samples[index].isSelected = bool; bool ? checkedSamples.add(samples[index]) : checkedSamples.remove(samples[index]); }); }), new Expanded( child: new Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ new Text("${samples[index].SerialNumber}", style: new TextStyle(fontSize: 18)), new Text( "样本名称:${samples[index].Name}", style: new TextStyle(color: Colors.black45), softWrap: false, overflow: TextOverflow.fade, ), new Text("可用量:${samples[index].AvailableVolume}", style: new TextStyle(color: Colors.black45)), new Text("位置:${samples[index].Location}", style: new TextStyle(color: Colors.black45)), ], )), ], ), ), ); } @override void querySamplesSuccess(SampleResult result) { //下拉刷新需要先清空列表数据 if (pageIndex == 1) { samples.clear(); checkedSamples.clear(); } samples.addAll(result.rows); //判断是不是最后一页 pageIndex * 20 >= result.total ? _controller.sendBack(false, RefreshStatus.noMore) : _controller.sendBack(false, RefreshStatus.idle); pageIndex += 1; setState(() {}); } @override void querySamplesFailed() { //查询失败,修改页面状态 _controller.sendBack(false, RefreshStatus.idle); loadStatus = PageLoadStatus.failed; setState(() {}); } @override void queryCartsSampleCountSuccess(int count) { setState(() { samplesCount = count; }); } @override void doJoinCartsFailed(String message) { Fluttertoast.showToast(msg: message); } @override void doJoinCartsSuccess(String message) { DialogUtil.showTips(context, text: message); _presenter.querySamples(pageIndex = 1); _presenter.queryCartsSampleCount(); } }
经过改动之后,会发现程序的类变多了,代码量视乎更大了。但是我们并不是以代码量的多少来评价代码的质量,往往是以代码的可阅读性和可变性来评价。经过改动之后,SampleClaimPage 类主要负责UI的实现和UI与数据的绑定及交互。ClaimPresenter类主要负责业务逻辑的实现和数据与UI之间交互的建立。而ClaimModel 类只需要简单的实现数据的获取。代码逻辑变得十分清晰。
结束语
代码模式的设计需要便于程序员理解代码,mvp模式特别适用于页面逻辑较为复杂的情况。当页面逻辑十分简单只时,就无需为了设计而设计,也就是代码界的一句金玉良言:“不要过度设计”。欢迎大家指正。