在 Android 開發中常常會用到一些架構,從 MVC 到 MVVP、MVVM等,這些架構會大大的解耦咱們代碼的功能模塊,讓咱們的代碼在項目中後期更容易擴展和維護。git
在Flutter中一樣有 MVC、MVP、MVVM等架構。在Android實際開發中,也有把項目從 MVC切換到 MVP,造成了一套 MVP 快速開發框架,且作了一個 AS 快速代碼生成插件。因此在 Flutter 開發中也想着是否是能夠用 MVP 架構去開發,且作個同樣的代碼生成插件。github
因此在這是裏主要看一下在 Flutter 中如何使用 MVP 模式來開發應用。api
提到MVP就不得不提到MVC,關於MVC架構,能夠看下面這張圖:bash
這種原理就會形成一個致命的缺陷:當不少業務邏輯寫在vidget中時,widget既充當了View層,又充當了Controller層。所以,耦合性極高,各類業務邏輯代碼和View代碼混合在一塊兒,你中有我我中有你,若是要修改一個需求,改動的地方可能至關多,維護起來十分不便。網絡
使用MVP模式會使得代碼多出一些接口,可是使得代碼邏輯更加清晰,尤爲是在處理複雜界面和邏輯時,能夠對同一個widget將每個業務都抽離成一個Presenter,這樣代碼既清晰邏輯明確又方便擴展。固然若是業務邏輯自己就比較簡單的話使用MVP模式就顯得沒那麼必要了。因此不須要爲了用它而用它,具體的仍是要根據業務須要。架構
簡而言之:view就是UI,model就是數據處理,而persenter則是他們的紐帶。app
可能存在的問題框架
所以,在進行MVP架構設計時須要考慮Presenter對View進行回傳時,View是否爲空?異步
Presenter與View什麼時候解除引用即Presenter可否和View層進行生命週期同步?async
好了,說了這麼多,我我的比較推薦mvp,主要是由於其相對比較簡單且易上手。下面咱們來看看具體如何優雅的實現MVP的封裝。
/// @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 用於取消網絡請求。
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,這樣就解決了上面說到的相互持有引用,形成內存泄漏問題。
/// @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 接口中定義了一些公共操做(加載狀態、無數據狀態、錯誤態、提交狀態、統一提示等)的方法,這裏你們能夠根據實際的須要是否須要定義這些公共方法。
AbstractView抽象類繼承StatefulWidget,AbstractViewState中定義一泛型P繼承 IPresenter,一泛型 V 繼承AbstractView,實現 IView,該抽象類中持有一個 Presenter 引用,且包括兩個生命週期方法initState、dispose用於建立、銷燬Presenter,並調用Presenter的attachView、detachView方法關聯 View、Model,並提供抽象createPresenter供子類實現。
這裏咱們以登陸功能模塊爲例:
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也只定義了登陸的方法。
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方法取消網絡請求。
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 爲空時指針問題。
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 首選項,找到 plugins , 選擇install plugin from disk,找到咱們剛下載的插件,重啓 IDE 生效。
在新建的 contract 類中快捷 Generate... 找到 FlutterMvpGenerator,就會生成對應模塊的 model、presenter、widget 類。
使用 MVP 模式,將使得應用更加好維護,同時也能夠方便咱們進行測試。
若是在使用過程遇到問題,歡迎下方留言交流。