在 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 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 介面中定義了一些公共操作(載入狀態、無資料狀態、錯誤態、提交狀態、統一提示等)的方法,這裡大家可以根據實際的需要是否需要定義這些公共方法,我這裡是預設是基類中處理,大家可參考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 {
@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 會額外增加一些介面、類,且它們的格式比較統一,為了統一規範程式碼,相關 MVP 的程式碼使用AS外掛來統一生成。
在 IDE中整合外掛
下載外掛下方外掛,開啟 IDE 首選項,找到 plugins , 選擇install plugin from disk,找到我們剛下載的外掛,重啟 IDE 生效。
生成程式碼
在新建的 contract 類中快捷 Generate... 找到 FlutterMvpGenerator,就會生成對應模組的 model、presenter、widget 類。
最後
使用 MVP 模式,將使得應用更加好維護,同時也可以方便我們進行測試。
如果在使用過程遇到問題,歡迎下方留言交流。