Обзор архитектур управления состоянием на Flutter

Привет! Меня зовут Антон Матрёнин, я — Senior Software Engineer в компании Avalanche Laboratory.

В последнее время рынок разработки стал пополняться кроссплатформенными проектами, которые должны выглядеть одинаково как на вебе, так и на Android/iOS. Одним из фреймворков, реализующим такую парадигму, является Flutter.

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

В этой статье мы рассмотрим четыре наиболее используемых подхода:

  • Native state
  • Provider (Scoped Model)
  • BLoC
  • Redux

Для демонстрации я создал простейшие приложения по архитектурным типам, каждое из которых состоит из двух экранов:

Первый экран:

  1. счетчик для демонстрации локального состояния;
  2. счетчик для демонстрации глобального состояния;
  3. кнопка инкремента локального и глобального состояния.

Второй экран:

  1. данные состояния с сайд-эффектами;
  2. счетчик для демонстрации синхронизации глобального состояния;
  3. кнопка инкремента глобального состояния.

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

Native state

Все, что вы используете во Flutter, состоит из виджетов. Они могут быть видимыми, невидимыми, содержать дочерние виджеты и взаимодействовать между собой. Каждый из них может быть как виджетом без состояния (Stateless Widget), так и виджетом, у которого есть состояние (Stateful Widget). Основное отличие —  возможность повторно отрисовывать виджеты во время выполнения приложения. Stateless Widget будет отрисовываться только один раз и является неизменяемым. Stateful Widget может отрисовываться множество раз в зависимости от изменения внутреннего состояния виджета.

Для создания Stateful Widget вам нужно создать 2 класса.

Первый класс должен наследоваться от Stateful Widget, который в свою очередь наследуется от Widget и является неизменяемым. Экземпляр этого класса не пересоздается при каждой отрисовке и используется для хранения переданных параметров и инициализации состояния.

Второй —  класс состояния, который имеет доступ к Stateful Widget через внутреннее свойство и занимается непосредственно отрисовкой состояния, реагируя на его изменение.

Глобальное состояние приложения, так же как и локальное, может быть реализовано с помощью Stateful Widget. Этот виджет создается в самом верхнем узле приложения и передается вниз по виджетам с помощью InheritedWidget. Каждый дочерний виджет приложения может получить доступ к виджету глобального состояния для изменения и использования его полей.

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

Преимущества и недостатки

Преимуществами такого подхода является простота и скорость внедрения в приложение с небольшим количеством экранов, которая выражается в отсутствии каких-либо дополнительных библиотек.

Из недостатков можно выделить такие:

  1. Представление и бизнес-логика никак не разделены.
  2. Сложность в модульном тестировании.
  3. Сложность в масштабировании и поддержке.
  4. Большое количество кода, который не может быть переиспользован.

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

Provider (Scoped Model)

Такой тип архитектуры позволяет вынести бизнес-логику из представления и дает возможность переиспользовать эту логику в разных модулях системы. Результат достигается с помощью создания модели и реагирования подписанных виджетов на ее изменение.

Изначально Brian Egan и Andrew Wilson разработали пакет scoped_model, который извлекли из кодовой базы Fuchsia и подвергли значительным улучшениям. Однако после Google I/O 2019 был представлен новый пакет, provider, который заменяет и улучшает Scoped Model, позволяя передавать модели вниз по дереву виджетов без ручного использования InheritedWidget.

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

Чтобы подписать виджет на модель, используется класс ChangeNotifierProvider, который является частью библиотеки provider. Подписка происходит непосредственно к тому виджету, который будет зависеть от данных из созданной модели.

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

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

Преимущества и недостатки

Из преимуществ можно выделить разделение бизнес-логики и представления с помощью создания моделей и event-based-архитектуры. Тестировать такие модели легче, чем в Native state, за счет отсутствия дополнительных усилий на создание виджетов, в которых это состояние используется. Немаловажно, что этот тип архитектуры поддерживается Google, так что можно не беспокоиться о популярности и поддержке этого решения.

Одна из основных проблем такой архитектуры — сложность в понимании того, какое свойство было изменено и с какой модели произошло уведомление виджетов об изменении. Вариантом решения этой проблемы является соглашение на уровне команды о введении так называемых экшенов. Это единственное место, где модель может вызвать метод уведомления либо другие экшены. Также вам нужно быть готовым, что многие вещи вроде persist-хранилища моделей недоступны из коробки и придется написать большое количество сопровождающего кода для их внедрения.

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

BLoC

BLoC (Business Logic Component) — шаблон, созданный Google для управления сложным состоянием приложения, основываясь на реактивной парадигме.

Основная идея заключается в том, что наше приложение разбито на модули, реализующие бизнес-логику. Каждый модуль имеет одну или несколько Sink (труб), которые являются некоторым входным потоком для агрегирования событий извне. В качестве выходных данных выступает Stream (поток), который определяет асинхронный формат данных для наших виджетов. Чтобы воспользоваться модулем на уровне виджета, применяют StreamBuilder, который управляет потоком данных и автоматически решает проблемы подписки и перерисовки дочернего дерева виджетов.

Несмотря на это, использовать BLoC в чистом виде — достаточно сложная работа, поскольку надо применять библиотеку RxDart для манипуляции с потоками, вручную отписываться от потоков, иначе можно получить серьезную утечку памяти на больших приложениях. С целью решения этих проблем была изобретена библиотека bloc от Феликса Ангелова, одного из разработчиков BMW Tech, который по максимуму упростил использование этого шаблона и предоставил удобное API для управления состоянием с возможностью легкого тестирования модулей. Немаловажным преимуществом этого пакета является возможность автогенерации кода с помощью плагинов для наиболее популярных IDE (IntelliJ, VS Code). Таким образом, мы не тратим времени на написание лишнего кода и имеем гибкость в изменении без лишней магии внутри.

Преимущества и недостатки

Сам подход интересный и имеет большое количество преимуществ:

  • богатое API при работе с потоками, что позволяет их легко группировать, совмещать и трансформировать;
  • группировка логики в одном месте;
  • легкость в тестировании состояния с сайд-эффектами за счет встроенного в Dart API тестирования потоков;
  • минимальное количество отрисовок благодаря использованию StreamBuilder.

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

Полная реализация проекта BLoC (library) на GitHub.

Redux

Почти все, кто пришел во Flutter из мира фронтенда (в частности, из React), знают о Flux-архитектуре и самой популярной ее реализации — Redux. Причины популярности просты:

  • Централизованность — состояние всего приложения находится в одном месте, что позволяет хранить его в любом удобном для вас хранилище.
  • Предсказуемость — не нужно императивно менять зависящие друг от друга модели, мы просто реагируем на действия, которые посылает нам система.
  • Простота отладки — всегда есть возможность посмотреть полное дерево состояния, а также возможность time-travel-отладки, когда вы можете последовательно пройтись по всем изменениям в стейте и своевременно найти и исправить ошибки.
  • Гибкость — существует большое количество middleware-расширений на все потребности программиста в управлении состоянием.

Некоторые Redux-реализации портированы на другие платформы, и Flutter не стал исключением. В dartpub есть пакет redux, который может быть использован как в вебе, так и на мобильных платформах с внедрением дополнительного пакета flutter_redux. Как мы уже говорили выше, в Redux нет разделения на локальное и глобальное. Состояние всегда глобальное — доступно любому виджету и доступно к изменению через экшены в любом месте системы.

Формально у нас существуют такие понятия:

  • State — модель состояния, которая может быть как скалярным, так и любым другим составным типом.
  • Action — класс-идентификатор события, который хранит в себе payload для передачи нужных параметров.
  • Reducer — обработчик экшенов, имеет доступ к текущему состоянию и экшену, который ждет обработки.
  • Dispatch — метод вызова экшена, который обрабатывается одним из редьюсеров.
  • Store — дерево состояния приложения, комбинирует в себе все редьюсеры, которые мы определили в приложении.
  • StoreConnector — дает возможность дочернему виджету получить доступ к store.

В чистой реализации Redux нет возможности работать с побочными эффектами. Обычно для этого используются дополнительные библиотеки redux-thunk, redux-saga и т. п.

В dartpub есть следующие пакеты для этих целей: redux_thunk и redux_epics.

Преимущества и недостатки

Работать с Redux достаточно удобно благодаря развивающимся сопровождающим библиотекам:

  • flutter_redux_dev_tools — отладка и time-travel debug;
  • redux_thunk — работа с сайд-эффектами с помощью thunk;
  • redux_epics — работа с сайд-эффектами с помощью эпиков, которые базируются на потоках;
  • redux_logging — логирование стейта или экшенов;
  • redux_persist_flutter — сохранение состояния в постоянном хранилище.

Однако недостатки наследуются от старшего родителя:

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

Заключение

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

Для небольших проектов или MVP лучшим решением, на мой взгляд, является Provider. С его помощью вы сможете легко и быстро внедрить необходимый бизнесу функционал. Для чего-то более серьезного — BLoC/Redux. Для каждого из них написано необходимое для комфортной работы количество библиотек и middleware, так что окончательный выбор будет зависеть от функционала, который предусматривается в приложении.

Все, кому интересно следить за Flutter, участвовать в обсуждении технических деталей и делиться опытом внедрения, присоединяйтесь к нашей группе Art Flutter в Telegram.

Полезные ссылки

Native state

Provider

BLoC

Redux


Читайте также: «Створення додатку на Flutter: перші кроки» и «Стоит ли инвестировать во Flutter. Сравнение Flutter и React Native»

Похожие статьи:
Німецький розробник програмного забезпечення SAP оголосив про надання безплатного доступу до своїх новітніх технологій університетам...
Привіт, мене звати Олег Шанковський, я Java-програміст. Працюю в Києві в американській компанії, що спеціалізується на кібербезпеці....
Популярный во всем мире сервис Skype открывает новые горизонты и возможности. Так, на днях была запущена новая версия функции Skype Translator,...
В этот раз DOU Ревизор побывал в TonicHealth — продуктовой IT-компании, основанной в 2010 году. Компания занимается разработкой ПО для...
Всем привет! Меня зовут Иван, я System Integration Architect в SoftServe, и в этой статье я хочу сделать обзор iPaaS (Integration Platforms as a Service) решения...
Яндекс.Метрика