Как стать автором
Обновить
110.04
Surf
Создаём веб- и мобильные приложения

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

Время на прочтение6 мин
Количество просмотров4.3K

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

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

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

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

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

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

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

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

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

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

  • фамилия, 

  • имя, 

  • отчество, 

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

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

  • интересы, 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Кнопка “Отмена”
Кнопка “Отмена”

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

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

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

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

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

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

Больше полезного про Flutter — в телеграм-канале Surf Flutter Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!

Больше кейсов команды Surf ищите на сайте >>

Теги:
Хабы:
Всего голосов 10: ↑9 и ↓1+8
Комментарии1

Публикации

Информация

Сайт
surf.ru
Дата регистрации
Дата основания
Численность
201–500 человек
Местоположение
Россия

Истории