Flutter MVP 封裝

  在 Android 開發中常常會用到一些架構,從 MVC 到 MVVP、MVVM等,這些架構會大大的解耦咱們代碼的功能模塊,讓咱們的代碼在項目中後期更容易擴展和維護。git

  在Flutter中一樣有 MVC、MVP、MVVM等架構。在Android實際開發中,也有把項目從 MVC切換到 MVP,造成了一套 MVP 快速開發框架,且作了一個 AS 快速代碼生成插件。因此在 Flutter 開發中也想着是否是能夠用 MVP 架構去開發,且作個同樣的代碼生成插件。github

  因此在這是裏主要看一下在 Flutter 中如何使用 MVP 模式來開發應用。api

MVC

  提到MVP就不得不提到MVC,關於MVC架構,能夠看下面這張圖:bash

  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則是他們的紐帶。app

可能存在的問題框架

  1. Model進行異步操做,獲取結果經過Presenter回傳到View時,出現View引用的空指針異常
  2. Presenter和View互相持有引用,解除不及時形成的內存泄漏。

所以,在進行MVP架構設計時須要考慮Presenter對View進行回傳時,View是否爲空?異步

Presenter與View什麼時候解除引用即Presenter可否和View層進行生命週期同步?async

  好了,說了這麼多,我我的比較推薦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 接口中定義了一些公共操做(加載狀態、無數據狀態、錯誤態、提交狀態、統一提示等)的方法,這裏你們能夠根據實際的須要是否須要定義這些公共方法。

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 模式,將使得應用更加好維護,同時也能夠方便咱們進行測試。

若是在使用過程遇到問題,歡迎下方留言交流。

Pub庫地址

插件地址

學習資料

請你們不吝點贊!由於您的點贊是對我最大的鼓勵,謝謝!

相關文章
相關標籤/搜索