Как подружить Elementary и BLoC

У каждого инструмента свои границы применимости, сильные и слабые стороны. Использовать решение в подходящей ситуации, а также комбинировать различные решения — хороший способ достичь эффективной разработки. Например, наша команда Surf удачно использует Elementary в связке с BLoC или Redux для управления бизнес-состоянием. 

Меня зовут Кристина Зотьева, я Flutter-разработчик. В этой статье вместе с Михаилом Зотьевым покажем один из примеров эффективного взаимодействия двух инструментов, которые могут удачно дополнить друг друга.

Проблематика

Elementary — архитектурный пакет, который позволяет разрабатывать приложение в парадигме MVVM-паттерна, чётко разделить слои по ответственностям. Поскольку именно в этом его непосредственная задача, внутри отсутствует строго продиктованный подход к управлению бизнес-состоянием: специально было оставлено пространство для манёвра в использовании. 

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

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

Код примера на Github >>

Естественно, для каждой ситуации нужно стараться использовать подходящий инструмент. Работу с бизнес-состоянием профиля хорошо можно описать конечным автоматом. Для этой части нашего приложения выберем BLoC. За презентационную логику, валидацию и тому подобные вещи будет отвечать Elementary. Приступим!

Формализация задания

Профиль пользователя состоит из типичных данных:  

  • фамилия,  

  • имя,  

  • отчество,  

  • дата рождения,  

  • место проживания,  

  • интересы,  

  • информация о себе. 

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

Профиль используется на нескольких экранах, отвечающих за различные его части:  

  • экран личной информации,  

  • экран выбора места жительства,

  • экран выбора интересов,

  • экран информации о себе. 

На экране личной информации находятся фамилия, имя, отчество, дата рождения. Заполнение всех полей обязательно, кроме отчества. Экран места жительства состоит из поля ввода с подсказкой городов от сервера по введенным данным. Также на этом экране представлена карта, где можно в интерактивном режиме выбрать место проживания. 

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

Основные экраныОсновные экраны

BLoC: описываем работу с бизнес-логикой

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

Чтобы описать эти состояния, нам понадобится набор сущностей — State. Самым первым состоянием будет состояние инициализации — InitProfileState, в котором мы ещё ничего не делали. Оно будет точкой входа BLoC — ProfileBloc. Из этого состояния нужно перейти в состояние, в котором есть данные о профиле, — ProfileState

У нас клиент-серверное приложение, и данные с сервера не могут прийти моментально. Поэтому добавляется промежуточное состояние, при котором идёт процесс загрузки профиля с сервера, — ProfileLoadingState. Из него, если загрузка будет успешной, мы перейдём в ProfileState. Но всегда положительных сценариев не бывает: что-то может сломаться и профиль не будет загружен. Следует добавить состояние для обработки такой ситуации — ErrorProfileLoadingStatе

Чтобы мы могли переключаться между этими состояниями, нужны определённые триггеры. Одним из них будет будет ProfileLoadEvent. В нашем случае этот Event применим к двум состояниям: когда мы только проинициализированы и когда у нас не получилось загрузить профиль с сервера. Из состояния загрузки в состояние загруженного профиля или состояние ошибки мы будем переходить автоматически, исходя из результата загрузки.

Таким образом от момента инициализации до получения профиля наш ProfileBloc выглядит так:

28060711a5a2213c289e468fc43f9373.png

После загрузки профиля мы можем его отредактировать. Чтобы описать состояние изменённого, но ещё не сохранённого профиля, используем PendingProfileState. Переход в это состояние осуществляется только из ProfileState с помощью триггера — ProfileUpdateEvent. Если изменения приводят к тому, что профиль идентичен изначально загруженному с сервера, мы окажемся в состоянии загруженного профиля.

После завершения изменений должна быть возможность сохранить профиль. Это делается запросом, и нам опять нужно состояние ожидания взаимодействия с сервером — SavingProfileState. Переходим в него по триггеру SaveProfileEvent. 

Сохранение, так же как и загрузка, может быть успешным и неуспешным. Поэтому из состояния сохранения мы можем перейти либо в состояние успешной загрузки — ProfileSaveSuccessfullyState, либо в состояние ошибки сохранения — ProfileSaveFailedState. 

Если сохранение успешно, автоматически переходим в состояние загруженного профиля — ProfileState. В случае ошибки можно повторно инициировать сохранение профиля или отменить изменения. Отменить изменения можно также и при состоянии измененного профиля. Триггером для этого события будет CancelEditingEvent.

Полная схема ProfileBloc выглядит так:

dfcdcdf83be518cb11f95f9af974fdec.png

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

Выделили четыре основных состояния:

  • IEditingAvailable — состояния, при которых доступно редактирование профиля. В нашем случае это PendingProfileState и ProfileState.

  • ILoadAvailable — состояния, при которых доступна загрузка профиля. Это InitProfileState, ErrorProfileLoadingState и ProfileState.

  • ICancelAvailable — состояние, при котором можно отменить изменения профиля: PendingProfileState и ProfileSaveFailedState.

  • ISaveAvailable — состояние, при котором доступно сохранение измененного профиля: PendingProfileState и ProfileSaveFailedState.

ad3d51b1d2836c7f760fe620bb997b8e.png

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

Elementary — для логики отображения и логики взаимодействия с пользователем

Бизнес-логику работы с профилем разобрали. Нам также нужна логика отображения и логика взаимодействия с пользователем. Их реализацию мы сделали, используя Elementary. 

Реализация экранов

Есть четыре основных экрана, на которых пользователь взаимодействует с профилем: их-то мы и реализуем на Elementary. В Elementary за бизнес-логику отвечает Model, поэтому Модели экранов принимают в качестве зависимости ProfileBloc и взаимодействуют с ним, добавляя нужные события. 

Виджет-модели этих экранов отвечают за то, что должно отображаться пользователю при открытии профиля в зависимости от заполненности и действий. Например, если пользователь просмотрел свой профиль, дошёл до последней страницы и не внёс никаких изменений, там будет кнопка «ОК». Пользователь нажмёт на неё и перейдёт на стартовую страницу. Если он внесёт изменения, на последней странице кнопка «ОК» заменится на кнопку «Save»: если нажать на неё, профиль будет сохранён. 

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

Процесс заполнения профиля / Кнопка OK=>Save» />Процесс заполнения профиля / Кнопка OK=>Save</p>

<h3>Реализация отдельных виджетов</h3>

<p>Маленькие виджеты тоже можно вынести в ElementаryWidget, потому что они могут иметь собственную бизнес-логику или логику отображения. </p>

<p>Например, кнопка,  позволяющая отменить все изменения и уйти на начальный экран, везде ведёт себя одинаково. У нас это CancelButton. Реализовывать её на каждом экране бессмысленно, в своей бизнес-логике она сообщает ProfileBloc, что нужно отменить изменения, и взаимодействует с навигацией.</p>

<p><img src=Кнопка «Отмена»

Ещё один пример — виджеты с обширной логикой отображения и собственной бизнес-логикой, которую можно изолировать от остальной бизнес-логики.

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

Для отображения выпадающего списка мы сделали OverlayEntryController: он отвечает за обновление визуального состояния подсказки и за её позиционирование относительно поля ввода.

При выборе города из подсказок выпадающий список пропадает, а в поле ввода появляется город, который выбрал пользователь.

Виджет выбора городаВиджет выбора города

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

© Habrahabr.ru