Обзор архитектур управления состоянием на 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»

Похожие статьи:
У свіжому випуску новинного дайджесту DOU News говоримо про чат-бот Bard від Google, проблеми ракети Starship від SpaceX, скорочення в GitLab, GitHub, PayPal, Zoom,...
Міністр цифрової трансформації Михайло Федоров спростував інформацію про те, що акаунти PayPal прийматимуть кошти лише до 30 червня...
Відбувся великий обмін полоненими (звільнено 86 українських військових), Туреччина готова надати судна для евакуації цивільних...
Цього тижня Україна зазнала нових обстрілів від російських військ — загарбники цілили по обʼєктах інфраструктури, внаслідок...
На рік компанія з російським корінням Flipper Devices випускає 40 тисяч хакерських інструментів Flipper Zero — це девайс, здатний...
Яндекс.Метрика