Русский
Русский
English
Статистика
Реклама

Model-Widget-WidgetModel, или какой архитектурой пользуется Flutter-команда в Surf

Привет, меня зовут Артём. Я руководитель Flutter-разработки в Surf и со-ведущий FlutterDev подкаста.


Flutter-отделу в Surf уже больше года. За это время мы сделали несколько проектов: от маленьких служебных, до полноценных е-коммерс и банкинга. Как минимум, многие из вас уже могли видеть приложение аптеки Ригла. В статье я расскажу про недавно вышедший пакет mwwm архитектуру, на которой построены все наши проекты.



Что такое MWWM?


MWWM это архитектура и реализация паттерна Model-View-ViewModel, которую мы в Surf переложили на Flutter. Мы заменили слово View на Widget, потому что View не очень часто используется во Flutter и так будет нагляднее для разработчиков. Главное, что она позволяет делать разделять вёрстку и логику, как бизнесовую, так и презентационного слоя.


Немного истории


Почему именно MWWM мы используем в Surf? Давайте вернёмся к началу 2019 года, когда у нас зародился Flutter отдел. Что было на тот момент?


Flutter зарелизился буквально месяц назад: хайп начал подниматься, активных игроков на рынке и в опенсорсе пока нет. Отличное время чтобы залететь, занять неплохое место в индустрии и завоевать внимание, не правда ли? Фреймворк молод, коммьюнити развивается и это открывает все дороги.


В это время от Android-отдела Surf отделяется Flutter направление. И сразу же стоит задача сначала заложить основу для наших будущих приложений, а потом уже кодить. Конечно же, одним из основных моментов здесь являлся выбор архитектуры.


В начале 2019 года нет явно выделяющегося мнения в комьюнити какую архитектуру использовать (хотя и сейчас ведутся активные холивары). Да, есть основные концепции: BLoC, Redux, Vanilla, MobX и тд. Самыми массовыми являются BLoC и Redux. Замечу, что речь ведётся не о пакетах, а о концепции.
Итак, встал вопрос BLoC или Redux нам взять? Или же придумать нечто своё?


Почему мы не BLoC?


Business Logic Component отличная концепция. Блоки кода, по факту, чёрные ящики с некоторым входным воздействием и выходным потоком, внутри которых крутится бизнес-логика на чистом Dart это просто потрясающе. Чистый Dart для кросс-платформенного переиспользования с вебом (да, тогда было далеко до Flutter for Web и сайты писали на Angular Dart, при необходимости). Удобство использования и достаточная изоляция самих блоков. Круто одним словом. Но есть одно но: где писать логику презентационного слоя? Где писать навигацию? Как работать с чисто UI событиями?


Чуть позже вышел в релиз всем известный Bloc от Felix Angelov. А также flutter_bloc. Многие стали рассматривать блоки как логику презентационного слоя, но это уже не вязалось с аббревиатурой. Сама же библиотека не давала указаний, где писать подобную логику, например, валидировать поля ввода. Это было неприемлемо для выбора хорошей архитектуры.


Redux?


Как я написал выше мы выходцы из Android разработки. В то время в мире царили Clean Architecture, подходил MVVM. Веб-технологии были чем-то чуждым и непонятным. Redux мы быстро отмели: на него надо было переучиваться довольно долго и отчасти менять мышление, к которому мы привыкли во время Android-разработки с Rx и CleanArchitecture.


Отбросив основные концепции и принимая во внимание реалии Surf, мы поставили задачу создать архитектуру, которая позволит Android-разработчикам из Surf при необходимости быстро конвертироваться во Flutter-разработчиков. И чтобы при необходимости Flutter-разработчики могли быстро перейти на Android, потому что архитектура понятна, знакома, а язык выучить несложно. Так и появился Model Widget WidgetModel.


Model-Widget-WidgetModel


Выглядит он примерно вот так.



Эту картинку можно видеть на странице в GitHub. Здесь есть несколько основных частей.


  • Widget-UI та самая вёрстка.
  • WidgetModel логика этого самого UI и его состояния.
  • Model контракт с сервисным слоем. На данный момент это экспериментальная часть. Исторически у нас использовались Interactorы напрямую в WM.

Пройдёмся по каждой из этих частей поподробнее.


Widget в нашем контексте полностью пассивная вёрстка. Максимум что допускается в ней наличие условного оператора, когда мы пишем там if (условие), покажи мне это, иначе покажи мне loader. А всё остальное: вычисления этих условий, обработка нажатий и так далее уходят в WidgetModel. Причём виджетом может быть как целый экран, так и конкретный маленький элемент на экране со своим состоянием.


Сразу замечу, кроме MwwmWidget (буду называть их так, чтобы не путаться) мы также используем обычные Flutter-виджеты. Потому что куда без них? Есть места, где нет необходимости усложнять простенький переключатель ради архитектуры.


WidgetModel это, по сути, состояние виджета и описание логики его работы. Что входит в логику? Это обработка тех или иных действий пользовательского интерфейса. Это обращение к любым другим слоям, которые стоят выше. Это вычисление тех или иных значений, обработка, mapping данных, необходимых для вёрстки. В общем-то, все то, чем должна заниматься стандартная ViewModel.


Рассмотрим на небольшом примере.



Допустим, у нас есть некоторый экран и у него есть некоторое состояние и кнопка, которая входит в сеть. Widget экрана будет содержать только вёрстку. При этом состояние его и реакция на взаимодействия в WidgetModel. Замечу, что WM описывает также некоторые микросостояния на экране. Этими микро-состояниями могут являться Streamы.


class SomeWidgetModel{    final _itemsController = StreamController<List<Items>>.broadcast();    get items => itemsController.stream;}

Внутри команды мы как раз используем стримы (а точнее обёртку над ними) внутри виджет-модели. Поэтому по форме она очень сильно похожа на конценпцию BLoCа. Это коробка с input/output. Каждый input это действие пользователя или событие, output данные, влияющие на UI. Как я уже сказал, каждый стрим микросостояние внутри виджет-модели. Такими состояниями могут быть маленькие элементы: кнопка, которая дизейблится при каких-то условиях; поток текста с экрана и т.д.


Вспомним о кнопке на экране. Её состояние может быть одним из таких стримов и тогда она просто будет обернута в StreamBuilder на экране. Но надо понимать, что в этом случае логика кнопки должна быть довольно проста.


Stream<bool> get isBtnDisabled => btnController.stream;

А теперь представим, что такая кнопка встречается ещё в 5 местах по всему приложению. И кроме некоторого изменения состояния на, к примеру, дизейбл, она ещё и триггерит запрос в Сеть. Причём запрос один и тот же, у него лишь разные аргументы. В этом случае каждый раз копипастить логику из экрана в экран, прописывать запросы и вообще загрязнять виджет-модель не очень хорошо. Гораздо лучше просто выделить эту кнопку в Widget+WidgetModel и целиком и полностью переиспользовать из экрана в экран, передавая те или иные параметры на вход.
Ещё одно важное замечание, что виджет-модель единственный способ привести виджет или часть виджета в некоторое состояние. Что под этим надо понимать? В Flutter, например, мы можем передавать некоторые аргументы виджет в конструктор. Это полезно, если вы используете stateless-виджеты.


Но что у нас получается при использовании WM? Самим состоянием виджета должна управлять именно виджет-модель. Это главный источник. Если передаём какие-то данные в виджет, то мы должны передать их в WM, и только из неё их взять. Напрямую эти данные использовать в виджете нельзя, потому что иначе получается два пути приведения виджета в состояние. И это вас в конечном счёте может запутать или привести к очень неочевидным багам, которых лучше не допускать.


Поток данных W-WM



Какой поток данных происходит между виджетом и виджет-моделью? Из виджета идут некоторые действия события пользовательского интерфейса. Из виджет-модели идут некоторые состояния в виджет. Состояния могут быть в виде потоков (так делаем мы в Surf), могут быть просто переменными. На эти состояния мы подписываем части виджета с помощью StreamBuilderов.


//child: StreamBuilder<Item>(  stream: wm.item,  builder: (ctx, snapshot) => //...),

Важно заметить, что связь между Widget и WidgetModel явно не прописана внутри пакета. Мы не стали сужать возможности фреймворка и дали свободу пользователям пакета самим определять связь. При этом, тот подход, что работает в нашей компании является отдельным пакетом на pub.dev.


Relation


Мы рекомендуем использовать наш пакет MWWM вместе с модулем Relation. Relation это связь, которую мы используем между виджетом виджет-моделью. Это просто семантическая обёртка над потоками. У нас есть некоторые потоковые состояния в виде StreamedState и действия под названием Action. С Relation довольно просто работать.


  final toggleAction = Action<int>();  final contentState = StreamedState<int>(0);//subscribe(toggleAction.stream, (data) => contentState.accept(data));

Обработка ошибок



В больших проектах очень важно правильно обрабатывать ошибки. В рамках MWWM предусмотрен специальный интерфейс ErrorHandler, который обязательно поставляется в WM. WidgetModel перехватывает ошибки, которые приходят из сервисного слоя (или происходят внутри презентационного), и передаёт их обработчику. Автоматическая обработка происходит при использовании методов WM с постфиксом ...HandleError().


subscribeHandleError(someAction, (data) => doOnData());doFutureHandleError(someFuture, (data) => doOnData());

Реализацию ErrorHandler можно посмотреть в примере проекта.


Model


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



Рассмотрим подробнее.


Model контракт и унифицированное АПИ для взаимодействие с сервисным слоем. По факту это конкретная сущность с двумя методами: perform и listen. При этом в модель передаётся некоторый список Performerов, но обо всём по порядку.


Суть взаимодействия основана на том, что WM говорит: Model, хочу внести Изменение (совершить действие) в сервисный слой. Сделай это и ждёт результата. Такая конструкция позволяет полностью абстрагироваться друг от друга при написании кода, просто условившись на контракте.


Change


Это первая часть контракта и это класс, который описывает намерение что-либо изменить, получить и тип получаемого результата. Он может содержать данные, но не может содержать никакой логики. Просто data-класс в терминах того же Kotlin.


class Authenticate extends FutureChange<Result> {  final String name;  Authenticate(this.name);}

Performer


Вторая часть контракта Performer. Performer это реализация логики. Самый близкий аналог перформера UseCase. Это такая функциональная часть контракта. Если Change это параметры, название метода, то Performer тело метода и, по сути, код, который исполняется по этому Change. В перформер могут поставляться любые сервисы, бизнес-логика, интеракторы и так далее. Такая конструкция полностью отвязывает виджет-модель от реализации этой бизнес-логики.
Идеальный перформер это только одно действие. То есть это такой очень атомарный кусок кода, который просто выполняет что-то одно и отдаёт результат. Performer однозначно связан с типом Change.


class AuthPerformer extends FuturePerformer<Result, Authenticate> {  final AuthService authService;  AuthPerformer(    // сущности, которые нужны перформеру для работы    this.authService,  );  Future<Result> perform(Authenticate change) {    return authService.login(change.name);  }}

Был один метод, стало два класса


Зачем это надо было разделять? Потому что если у интерактора был один метод, то здесь у нас получается два класса вместо одного метода. Но таким образом из виджет-модели исчезает полностью информация об интеракторах, о реализации сервисного слоя. Всё что нужно знать, что вы хотите сделать. То есть знать Change. И предоставить Model с необходимым набором перформеров.


Неприятный минус: если вы не предоставили Performer, то узнаете об этом только в рантайме.


Бизнес-логика


Тут полная свобода действий. MWWM не декларирует, как реализовать бизнес-логику вашего приложения. Рекомендуется использовать тот подход, что принят в вашей команде. В Surf мы используем CleanArchitecture, я в своём pe-проекте работаю с сервисами, которые поставляются в перформеры и там работают. Тут может быть действительно всё, что угодно. Вся суть MWWM в том, что он довольно гибок в использовании, и его можно адаптировать под свою команду.


Стек Surf


Наш стек в Surf это вот такой набор пакетов для архитектуры.



По факту для нас это один пакет под названием surf_mwwm. Если интересно посмотреть подробнее, то можно найти на нашем GitHub.


На диаграмме:


  • injector пакет для реализации DI. Основан на InheritedWidget. Маленький и простой.
  • relation связь между слоями.
  • mwwm герой этой статьи
  • surf_mwwm всё вместе с небольшой добавочкой, специфичной для нашей команды.

Вместо заключения


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


Человеку, который пишет проект в одиночку, это может показаться сильным оверхедом и замедлением разработки. Можно написать гораздо быстрее на других фреймворках, где всё поставлено на рельсы, в ущерб гибкости и кастомизации. Здесь же больше свободы и гибкости для адаптации под свои нужды.


Но в любом случае этот подход позволяет полностью разделить все слои, что явно улучшает читаемость и поддержку написанного кода.


Если вкратце, что мы имеем по MWWM:


Плюсы:


  • Разделяет и изолирует все слои (UI, Presentation Logic, Business Logic).
  • Не имеет лишних зависимостей.
  • Гибко настраивается под нужды команды.
  • Позволяет описать контракт и работать параллельно.

Минусы:


  • Слишком сложно, если пишешь в одного.
  • Многословное описание контракта.

MWWM это часть нашего большего репозитория. У неё есть свой отдельный репозиторий SurfGear. Это набор наших стандартов и библиотек, которые мы используем в Surf.


Часть этих библиотек уже в релизе на pub.dev:



И команда Surf не собирается останавливаться на этом.

Источник: habr.com
К списку статей
Опубликовано: 09.07.2020 18:19:56
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании surf

Dart

Flutter

Разработка мобильных приложений

Surf

Mobile

Architecture

Mobile development

Категории

Последние комментарии

© 2006-2020, personeltest.ru