Привет, меня зовут Артём. Я руководитель Flutter-разработки в Surf и со-ведущий FlutterDev подкаста.  Flutter-отделу в Surf уже больше года. За это время мы сделали несколько проектов: от маленьких служебных, до полноценных е-коммерс и банкинга. Как минимум, многие из вас уже могли видеть приложение аптеки «Ригла». В статье я расскажу про недавно вышедший пакет 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 нам взять? Или же придумать нечто своё? Business Logic Component — отличная концепция. Блоки кода, по факту, «чёрные ящики» с некоторым входным воздействием и выходным потоком, внутри которых крутится бизнес-логика на чистом Dart — это просто потрясающе. Чистый Dart для кросс-платформенного переиспользования с вебом (да, тогда было далеко до Flutter for Web и сайты писали на Angular Dart, при необходимости). Удобство использования и достаточная изоляция самих блоков. Круто одним словом. Но есть одно «но»: где писать логику презентационного слоя? Где писать навигацию? Как работать с чисто UI событиями? Чуть позже вышел в релиз всем известный Bloc от Felix Angelov. А также flutter_bloc. Многие стали рассматривать блоки как логику презентационного слоя, но это уже не вязалось с аббревиатурой. Сама же библиотека не давала указаний, где писать подобную логику, например, валидировать поля ввода. Это было неприемлемо для выбора хорошей архитектуры. Как я написал выше — мы выходцы из Android разработки. В то время в мире царили Clean Architecture, подходил MVVM. Веб-технологии были чем-то чуждым и непонятным. Redux мы быстро отмели: на него надо было переучиваться довольно долго и отчасти менять мышление, к которому мы привыкли во время Android-разработки с Rx и CleanArchitecture. Отбросив основные концепции и принимая во внимание реалии Surf, мы поставили задачу создать архитектуру, которая позволит Android-разработчикам из Surf при необходимости быстро конвертироваться во Flutter-разработчиков. И чтобы при необходимости Flutter-разработчики могли быстро перейти на Android, потому что архитектура понятна, знакома, а язык выучить несложно. Так и появился Model – Widget – WidgetModel. Выглядит он примерно вот так.   Эту картинку можно видеть на странице в GitHub. Здесь есть несколько основных частей.  Пройдёмся по каждой из этих частей поподробнее.  Widget в нашем контексте — полностью пассивная вёрстка. Максимум что допускается в ней – наличие условного оператора, когда мы пишем там if (условие), покажи мне это, иначе – покажи мне loader. А всё остальное: вычисления этих условий, обработка нажатий и так далее — уходят в WidgetModel. Причём виджетом может быть как целый экран, так и конкретный маленький элемент на экране со своим состоянием. Сразу замечу, кроме MwwmWidget (буду называть их так, чтобы не путаться) мы также используем обычные Flutter-виджеты. Потому что куда без них? Есть места, где нет необходимости усложнять простенький переключатель ради архитектуры. WidgetModel – это, по сути, состояние виджета и описание логики его работы. Что входит в логику? Это обработка тех или иных действий пользовательского интерфейса. Это обращение к любым другим слоям, которые стоят выше. Это вычисление тех или иных значений, обработка, mapping данных, необходимых для вёрстки. В общем-то, все то, чем должна заниматься стандартная ViewModel. Рассмотрим на небольшом примере.   Допустим, у нас есть некоторый экран и у него есть некоторое состояние и кнопка, которая входит в сеть. Widget экрана будет содержать только вёрстку. При этом состояние его и реакция на взаимодействия в WidgetModel. Замечу, что WM описывает также некоторые микросостояния на экране. Этими микро-состояниями могут являться Stream’ы. Внутри команды мы как раз используем стримы (а точнее обёртку над ними) внутри виджет-модели. Поэтому по форме она очень сильно похожа на конценпцию BLoC’а. Это коробка с input/output. Каждый input — это действие пользователя или событие, output — данные, влияющие на UI. Как я уже сказал, каждый стрим — микросостояние внутри виджет-модели. Такими состояниями могут быть маленькие элементы: кнопка, которая дизейблится при каких-то условиях; поток текста с экрана и т.д. Вспомним о кнопке на экране. Её состояние может быть одним из таких стримов и тогда она просто будет обернута в StreamBuilder на экране. Но надо понимать, что в этом случае логика кнопки должна быть довольно проста. А теперь представим, что такая кнопка встречается ещё в 5 местах по всему приложению. И кроме некоторого изменения состояния на, к примеру, дизейбл, она ещё и триггерит запрос в Сеть. Причём запрос один и тот же, у него лишь разные аргументы. В этом случае каждый раз копипастить логику из экрана в экран, прописывать запросы и вообще загрязнять виджет-модель не очень хорошо. Гораздо лучше просто выделить эту кнопку в Widget+WidgetModel и целиком и полностью переиспользовать из экрана в экран, передавая те или иные параметры на вход.
Ещё одно важное замечание, что виджет-модель – единственный способ привести виджет или часть виджета в некоторое состояние. Что под этим надо понимать? В Flutter, например, мы можем передавать некоторые аргументы виджет в конструктор. Это полезно, если вы используете stateless-виджеты.  Но что у нас получается при использовании WM? Самим состоянием виджета должна управлять именно виджет-модель. Это главный источник. Если передаём какие-то данные в виджет, то мы должны передать их в WM, и только из неё их взять. Напрямую эти данные использовать в виджете нельзя, потому что иначе получается два пути приведения виджета в состояние. И это вас в конечном счёте может запутать или привести к очень неочевидным багам, которых лучше не допускать.
  Какой поток данных происходит между виджетом и виджет-моделью? Из виджета идут некоторые действия — события пользовательского интерфейса. Из виджет-модели идут некоторые состояния в виджет. Состояния могут быть в виде потоков (так делаем мы в Surf), могут быть просто переменными. На эти состояния мы подписываем части виджета с помощью StreamBuilder’ов.  Важно заметить, что связь между Widget и WidgetModel явно не прописана внутри пакета. Мы не стали сужать возможности фреймворка и дали свободу пользователям пакета самим определять связь. При этом, тот подход, что работает в нашей компании является отдельным пакетом на pub.dev. Мы рекомендуем использовать наш пакет MWWM вместе с модулем Relation. Relation – это связь, которую мы используем между виджетом виджет-моделью. Это просто семантическая обёртка над потоками. У нас есть некоторые потоковые состояния в виде StreamedState и действия под названием Action. С Relation довольно просто работать.   В больших проектах очень важно правильно обрабатывать ошибки. В рамках MWWM предусмотрен специальный интерфейс ErrorHandler, который обязательно поставляется в WM. WidgetModel перехватывает ошибки, которые приходят из сервисного слоя (или происходят внутри презентационного), и передаёт их обработчику. Автоматическая обработка происходит при использовании методов WM с постфиксом ...HandleError(). Реализацию ErrorHandler можно посмотреть в примере проекта. Model – это та самая экспериментальная и недавно появившаяся фича, которая пока что опциональна в рамках архитектуры. Это описание контракта между виджет-моделью и бизнес-логикой приложения.   Рассмотрим подробнее.  Model — контракт и унифицированное АПИ для взаимодействие с сервисным слоем. По факту это конкретная сущность с двумя методами: perform и listen. При этом в модель передаётся некоторый список Performer’ов, но обо всём по порядку. Суть взаимодействия основана на том, что WM говорит: «Model, хочу внести Изменение (совершить действие) в сервисный слой. Сделай это» и ждёт результата. Такая конструкция позволяет полностью абстрагироваться друг от друга при написании кода, просто условившись на контракте. Это первая часть контракта и это класс, который описывает намерение что-либо изменить, получить и тип получаемого результата. Он может содержать данные, но не может содержать никакой логики. Просто data-класс в терминах того же Kotlin. Вторая часть контракта – Performer. Performer – это реализация логики. Самый близкий аналог перформера — UseCase. Это такая функциональная часть контракта. Если Change – это параметры, название метода, то Performer – тело метода и, по сути, код, который исполняется по этому Change. В перформер могут поставляться любые сервисы, бизнес-логика, интеракторы и так далее. Такая конструкция полностью отвязывает виджет-модель от реализации этой бизнес-логики.
Идеальный перформер – это только одно действие. То есть это такой очень атомарный кусок кода, который просто выполняет что-то одно и отдаёт результат. Performer однозначно связан с типом Change. Зачем это надо было разделять? Потому что если у интерактора был один метод, то здесь у нас получается два класса вместо одного метода. Но таким образом из виджет-модели исчезает полностью информация об интеракторах, о реализации сервисного слоя. Всё что нужно – знать, что вы хотите сделать. То есть знать Change. И предоставить Model с необходимым набором перформеров. Неприятный минус: если вы не предоставили Performer, то узнаете об этом только в рантайме. Тут полная свобода действий. MWWM не декларирует, как реализовать бизнес-логику вашего приложения. Рекомендуется использовать тот подход, что принят в вашей команде. В Surf мы используем CleanArchitecture, я в своём pet-проекте работаю с сервисами, которые поставляются в перформеры и там работают. Тут может быть действительно всё, что угодно. Вся суть MWWM в том, что он довольно гибок в использовании, и его можно адаптировать под свою команду. Наш стек в Surf – это вот такой набор пакетов для архитектуры.   По факту для нас это один пакет под названием surf_mwwm. Если интересно посмотреть подробнее, то можно найти на нашем GitHub. На диаграмме: Надеюсь, данный подход и пакет поможет командам. Особенно большим, потому что тут можно, описав контракты между слоями, параллельно работать над фичами, и даже никак не сталкиваться в работе над одними и теми же файлами. Данный факт позволяет быть автономными. Человеку, который пишет проект в одиночку, это может показаться сильным оверхедом и замедлением разработки. Можно написать гораздо быстрее на других фреймворках, где всё поставлено на рельсы, в ущерб гибкости и кастомизации. Здесь же больше свободы и гибкости для адаптации под свои нужды. Но в любом случае этот подход позволяет полностью разделить все слои, что явно улучшает читаемость и поддержку написанного кода. Если вкратце, что мы имеем по MWWM:  Плюсы:  Минусы:  MWWM – это часть нашего большего репозитория. У неё есть свой отдельный репозиторий — SurfGear. Это набор наших стандартов и библиотек, которые мы используем в Surf.  Часть этих библиотек уже в релизе на pub.dev:  И команда Surf не собирается останавливаться на этом. Head of Flutter Department