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模式特別適用於頁面邏輯較為復雜的情況。當頁面邏輯十分簡單只時,就無需為了設計而設計,也就是代碼界的一句金玉良言:“不要過度設計”。歡迎大家指正。