У каждого инструмента свои границы применимости, сильные и слабые стороны. Использовать решение в подходящей ситуации, а также комбинировать различные решения — хороший способ достичь эффективной разработки. Например, наша команда Surf удачно использует Elementary в связке с BLoC или Redux для управления бизнес-состоянием.
Меня зовут Кристина Зотьева, я Flutter-разработчик. В этой статье вместе с Михаилом Зотьевым покажем один из примеров эффективного взаимодействия двух инструментов, которые могут удачно дополнить друг друга.
Проблематика
Elementary — архитектурный пакет, который позволяет разрабатывать приложение в парадигме MVVM-паттерна, чётко разделить слои по ответственностям. Поскольку именно в этом его непосредственная задача, внутри отсутствует строго продиктованный подход к управлению бизнес-состоянием: специально было оставлено пространство для манёвра в использовании.
Отсюда вытекает закономерный и часто задаваемый вопрос об Elementary: как подружить его со стейт-менеджером. Поскольку модель является лишь самым верхним слоем, включающим бизнес-логику, она может реализовать её самостоятельно или передать управление в глубину другим ответственным. Это важный момент: он позволяет раскрыть не только вышеозначенный вопрос, но ещё и ситуацию, когда, например, некоторая логика распространяется на несколько различных экранов.
В этой статье разберём как раз такой пример: поработаем с профилем пользователя с нескольких экранов. На них будем отображать различные данные этого профиля и дадим возможность менять их.
Естественно, для каждой ситуации нужно стараться использовать подходящий инструмент. Работу с бизнес-состоянием профиля хорошо можно описать конечным автоматом. Для этой части нашего приложения выберем 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»: если нажать на неё, профиль будет сохранён.
Также виджет-модели отвечают за валидацию полей, реагируют на действия пользователя и передают данные в модель, а модель, в свою очередь, блоку.
Реализация отдельных виджетов
Маленькие виджеты тоже можно вынести в ElementаryWidget, потому что они могут иметь собственную бизнес-логику или логику отображения.
Например, кнопка, позволяющая отменить все изменения и уйти на начальный экран, везде ведёт себя одинаково. У нас это CancelButton. Реализовывать её на каждом экране бессмысленно, в своей бизнес-логике она сообщает ProfileBloc, что нужно отменить изменения, и взаимодействует с навигацией.
Ещё один пример — виджеты с обширной логикой отображения и собственной бизнес-логикой, которую можно изолировать от остальной бизнес-логики.
В нашем примере это виджет FieldWithSuggestionsWidget. Он нужен для поиска и выбора города из предложенного списка. Когда пользователь вводит город, модель этого виджета отправляет запрос на сервер с введёнными данными и получает ответ в виде списка предложенных городов. Он отображается в выпадающем списке. Пока идёт запрос на сервер, крутится Loader.
Для отображения выпадающего списка мы сделали OverlayEntryController: он отвечает за обновление визуального состояния подсказки и за её позиционирование относительно поля ввода.
При выборе города из подсказок выпадающий список пропадает, а в поле ввода появляется город, который выбрал пользователь.
Надеемся, что статья помогла разобраться с вопросами шаринга части бизнес-логики между экранами Elementary и взаимодействия с другими эффективными в своей части инструментами. Если вам интересен наш подход, приглашаем ознакомиться с другими статьями на эту тему:
Больше полезного про Flutter — в телеграм-канале Surf Flutter Team. Публикуем кейсы, лучшие практики, новости и вакансии Surf, а также проводим прямые эфиры. Присоединяйтесь!
Больше кейсов команды Surf ищите на сайте >>