Упаковка Flutter MVP

Flutter

  Некоторые архитектуры часто используются в разработке для Android, от MVC до MVVP, MVVM и т. д. Эти архитектуры значительно разъединяют функциональные модули нашего кода, упрощая расширение и поддержку нашего кода на средних и поздних этапах проекта.

   Во Flutter также есть MVC, MVP, MVVM и другие архитектуры. В ходе фактической разработки Android некоторые проекты также были переведены с MVC на MVP, сформирован набор фреймворков быстрой разработки MVP и создан плагин AS для быстрой генерации кода. Поэтому при разработке Flutter я также задавался вопросом, можно ли его разработать с архитектурой MVP, и сделал такой же плагин генерации кода.

   Итак, здесь мы в основном рассмотрим, как использовать шаблон MVP для разработки приложений во Flutter.

MVC

  Когда вы упоминаете MVP, вы должны упомянуть MVC.Для архитектуры MVC вы можете увидеть следующую картину:

  MVC — это Контроллер представления модели.Короче говоря, он манипулирует данными уровня модели посредством управления контроллером и возвращает их на уровень представления для отображения, как показано на рисунке выше. Когда пользователь инициирует событие, уровень представления отправляет инструкцию на уровень контроллера, а затем контроллер уведомляет уровень модели об обновлении данных.После того, как уровень модели обновит данные, они будут отображаться непосредственно на уровне представления. , Вот как работает MVC.

   Этот принцип повлечет за собой фатальный недостаток: когда в виджете написано много бизнес-логики, виджет действует как слой представления и слой контроллера. Таким образом, связь чрезвычайно высока, и различные коды бизнес-логики и коды представления смешиваются друг с другом. У вас есть я, а у меня есть вы. Если вы хотите изменить требование, может быть много изменений, которые очень неудобно поддерживать. .

MVP

Режим MVP эквивалентен добавлению Presenter в режим MVC для обработки модели и логики и полного разделения представления и модели.Воплощение в разработке флаттера заключается в том, что виджет используется только для отображения интерфейса и взаимодействия, а виджет не участвует в структуре и логике модели.

Использование режима MVP сделает код более интерфейсным, но сделает логику кода более понятной, особенно при работе со сложными интерфейсами и логикой, вы можете извлечь каждое дело в Presenter для одного и того же виджета, чтобы код был понятным и логичным. легко расширить. Конечно, если сама бизнес-логика относительно проста, нет необходимости использовать паттерн MVP. Таким образом, вам не нужно использовать его просто для использования, это зависит от потребностей вашего бизнеса.

   Вкратце: представление — это пользовательский интерфейс, модель — это обработка данных, а персистент — их связующее звено.

Потенциальная проблема

  1. Когда модель выполняет асинхронную операцию и результат возвращается в представление через презентатор, возникает исключение нулевого указателя, на которое ссылается представление.
  2. Presenter и View содержат ссылки друг на друга и устраняют утечку памяти, вызванную несвоевременным освобождением.

Следовательно, при разработке архитектуры MVP необходимо учитывать, пусто ли представление, когда презентатор возвращает представление?

Когда будут разыменованы Presenter и View, то есть смогут ли Presenter и View синхронизировать жизненный цикл?

   Что ж, сказав так много, я лично рекомендую mvp, главным образом потому, что он относительно прост и удобен в использовании. Давайте посмотрим, как элегантно реализовать инкапсуляцию 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 имеет абстрактное расположение, которое в основном используется для выпуска сетевых запросов.

Абстрактный класс AbstractModel реализует интерфейс IModel, а в конструкторе создается уникальный тег для отмены сетевого запроса.

См. конец для конкретного кода

Настоящий пакет

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();
}

Общий V устанавливается в интерфейсе IPresenter для наследования IView, V — это представление, связанное с презентатором, и есть два абстрактных метода attachView и detachView.

Абстрактный класс AbstractPresenter настроен с помощью универсального V, наследующего IView, универсального M, наследующего IModel, и реализующего IPresenter, который содержит ссылку на представление и ссылку на модель. Представление привязано к attachView, и абстрактный метод для создания объекта Model генерируется для реализации подклассами, а представление и модель уничтожаются в detachView, что решает вышеупомянутое удержание взаимных ссылок и вызывает проблему утечки памяти. .

См. конец для конкретного кода

Посмотреть пакет

/// @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 определяет некоторые методы для общедоступных операций (состояние загрузки, состояние отсутствия данных, состояние ошибки, состояние отправки, унифицированное приглашение и т. д.). Здесь вы можете определить эти общедоступные методы в соответствии с фактическими потребностями. Я здесь по умолчанию. Базовый класс процесс, вы можете обратиться кПакет BaseWidget базового класса Flutter.

Абстрактный класс AbstractView наследует StatefulWidget, AbstractViewState определяет универсальный P, наследующий IPresenter, универсальный V наследует AbstractView, реализует IView, этот абстрактный класс содержит ссылку на Presenter и включает два метода жизненного цикла: initState, dispose for create, Destroy the Presenter и вызов attachView и detachView класса Presenter, чтобы связать 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);
}

Здесь определяются интерфейс просмотра, интерфейс модели и интерфейс презентатора страницы входа.

В представлении определяются только методы, связанные с отображением пользовательского интерфейса, такие как успешный вход в систему.

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,
    );
  }
}

Создайте здесь класс реализации модели, перепишите метод входа в систему, чтобы передать результат возврата интерфейса входа в систему обратного вызова, и перепишите метод удаления, чтобы отменить сетевой запрос.

Ведущий класс

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, реализуйте метод входа в систему, вызовите метод входа в модель в модели, получите данные в обратном вызове, сделайте некоторые логические суждения и передайте результат соответствующему методу представления.

Обратите внимание, что представление?. используется здесь для решения проблемы с указателем, когда представление пусто.

Класс виджета

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);
  }
  
}

Логин здесь — это представление функционального модуля входа в систему, наследующее BaseWidget и переходящее в общее представление и презентатор. Реализуйте интерфейс LoginContract.View и перепишите методы пользовательского интерфейса, определенные интерфейсом.

Создайте объект LoginPresenter в методе createPresenter и верните его. Таким образом, вы можете использовать getPresenter для прямого управления логикой.

плагин кода

Использование MVP добавит некоторые дополнительные интерфейсы и классы, а их форматы относительно унифицированы.В целях стандартизации кода код соответствующего MVP генерируется единообразно с помощью подключаемого модуля AS.

Интеграция плагинов в IDE

Загрузите плагин под плагином, откройте настройки IDE, найдите плагины, выберите «Установить плагин с диска», найдите плагин, который мы только что скачали, перезапустите IDE, чтобы он вступил в силу.

сгенерировать код

Быстрое создание... Найдите FlutterMvpGenerator во вновь созданном классе контракта, и будут сгенерированы классы модели, презентатора и виджета соответствующего модуля.

Наконец

Использование шаблона MVP сделает приложение более удобным в сопровождении, а также облегчит наше тестирование.

Если вы столкнулись с проблемами во время использования, пожалуйста, оставьте сообщение ниже, чтобы сообщить.

Адрес паб-библиотеки

Адрес плагина

учебные материалы

Пожалуйста, поднимите палец вверх! Потому что ваши лайки - моя самая большая поддержка, спасибо!