Привет! Меня зовут Антон Матрёнин, я — Senior Software Engineer в компании Avalanche Laboratory.
В последнее время рынок разработки стал пополняться кроссплатформенными проектами, которые должны выглядеть одинаково как на вебе, так и на Android/iOS. Одним из фреймворков, реализующим такую парадигму, является Flutter.
Многие уже слышали о Flutter, рассматривали преимущества и недостатки и даже пробовали создавать свой первый проект. Самое время поговорить о сердце любого приложения — архитектуре управления состоянием.
В этой статье мы рассмотрим четыре наиболее используемых подхода:
Первый экран:
Для демонстрации подходов и легкого понимания отлично подойдут максимально упрощенные примеры. Именно поэтому мною были использованы примеры со счетчиком. В конце статьи будет подведен небольшой итог по выбору того или иного подхода с перечнем ссылок для глубокого погружения. А ссылки на репозитории каждого проекта будут приведены в соответствующем разделе.
Для создания Stateful Widget вам нужно создать 2 класса.
Первый класс должен наследоваться от Stateful Widget, который в свою очередь наследуется от Widget и является неизменяемым. Экземпляр этого класса не пересоздается при каждой отрисовке и используется для хранения переданных параметров и инициализации состояния.
Второй — класс состояния, который имеет доступ к Stateful Widget через внутреннее свойство и занимается непосредственно отрисовкой состояния, реагируя на его изменение.
Глобальное состояние приложения, так же как и локальное, может быть реализовано с помощью Stateful Widget. Этот виджет создается в самом верхнем узле приложения и передается вниз по виджетам с помощью InheritedWidget. Каждый дочерний виджет приложения может получить доступ к виджету глобального состояния для изменения и использования его полей.
Для управления состояниями с сайд-эффектами мы руководствуемся теми же принципами. Вызываем асинхронную функцию, которая последовательно устанавливает флаг для индикации о загрузке с сервера, выполняем нужный нам запрос на сервер и меняем состояние в зависимости от пришедших данных.
Из недостатков можно выделить такие:
Изначально Brian Egan и Andrew Wilson разработали пакет scoped_model, который извлекли из кодовой базы Fuchsia и подвергли значительным улучшениям. Однако после Google I/O 2019 был представлен новый пакет, provider, который заменяет и улучшает Scoped Model, позволяя передавать модели вниз по дереву виджетов без ручного использования InheritedWidget.
Для локального состояния виджета первым делом необходимо создать модель со всеми полями, которые будут использоваться в вашем виджете. После изменения каждого поля (или полей) вам нужно сообщить подписчикам, что модель изменилась, и выполнить отрисовку подписанных виджетов.
Чтобы подписать виджет на модель, используется класс ChangeNotifierProvider, который является частью библиотеки provider. Подписка происходит непосредственно к тому виджету, который будет зависеть от данных из созданной модели.
Глобальное состояние ничем не отличается от локального, кроме того, что модель подписывается к самому верхнему виджету приложения. Имейте в виду, что сама модель должна иметь уникальный класс, чтобы не было конфликтов при ее поиске в дочерних виджетах.
В модели для хранения состояния с сайд-эффектами необходимо предусмотреть специальный метод, который установит индикатор начала асинхронной загрузки данных с сервера и установит данные, пришедшие с сервера, сбросив при этом индикатор загрузки. Однако здесь кроется проблема двойного уведомления подписчиков на изменение модели. Первое уведомление происходит во время изменения установки данных с сервера, второе — после изменения индикатора загрузки.
Одна из основных проблем такой архитектуры — сложность в понимании того, какое свойство было изменено и с какой модели произошло уведомление виджетов об изменении. Вариантом решения этой проблемы является соглашение на уровне команды о введении так называемых экшенов. Это единственное место, где модель может вызвать метод уведомления либо другие экшены. Также вам нужно быть готовым, что многие вещи вроде persist-хранилища моделей недоступны из коробки и придется написать большое количество сопровождающего кода для их внедрения.
Provider отлично подходит для средних проектов, в которых нет большого зацепления между модулями. Полная реализация проекта на GitHub.
Основная идея заключается в том, что наше приложение разбито на модули, реализующие бизнес-логику. Каждый модуль имеет одну или несколько Sink (труб), которые являются некоторым входным потоком для агрегирования событий извне. В качестве выходных данных выступает Stream (поток), который определяет асинхронный формат данных для наших виджетов. Чтобы воспользоваться модулем на уровне виджета, применяют StreamBuilder, который управляет потоком данных и автоматически решает проблемы подписки и перерисовки дочернего дерева виджетов.
Несмотря на это, использовать BLoC в чистом виде — достаточно сложная работа, поскольку надо применять библиотеку RxDart для манипуляции с потоками, вручную отписываться от потоков, иначе можно получить серьезную утечку памяти на больших приложениях. С целью решения этих проблем была изобретена библиотека bloc от Феликса Ангелова, одного из разработчиков BMW Tech, который по максимуму упростил использование этого шаблона и предоставил удобное API для управления состоянием с возможностью легкого тестирования модулей. Немаловажным преимуществом этого пакета является возможность автогенерации кода с помощью плагинов для наиболее популярных IDE (IntelliJ, VS Code). Таким образом, мы не тратим времени на написание лишнего кода и имеем гибкость в изменении без лишней магии внутри.
Полная реализация проекта BLoC (library) на GitHub.
Формально у нас существуют такие понятия:
В чистой реализации Redux нет возможности работать с побочными эффектами. Обычно для этого используются дополнительные библиотеки redux-thunk, redux-saga и т. п.
В dartpub есть следующие пакеты для этих целей: redux_thunk и redux_epics.
Для небольших проектов или MVP лучшим решением, на мой взгляд, является Provider. С его помощью вы сможете легко и быстро внедрить необходимый бизнесу функционал. Для чего-то более серьезного — BLoC/Redux. Для каждого из них написано необходимое для комфортной работы количество библиотек и middleware, так что окончательный выбор будет зависеть от функционала, который предусматривается в приложении.
Источник статьи: https://dou.ua/lenta/articles/flutter-architecture/
В последнее время рынок разработки стал пополняться кроссплатформенными проектами, которые должны выглядеть одинаково как на вебе, так и на Android/iOS. Одним из фреймворков, реализующим такую парадигму, является Flutter.
Многие уже слышали о Flutter, рассматривали преимущества и недостатки и даже пробовали создавать свой первый проект. Самое время поговорить о сердце любого приложения — архитектуре управления состоянием.
В этой статье мы рассмотрим четыре наиболее используемых подхода:
- Native state
- Provider (Scoped Model)
- BLoC
- Redux
Первый экран:
- счетчик для демонстрации локального состояния;
- счетчик для демонстрации глобального состояния;
- кнопка инкремента локального и глобального состояния.
- данные состояния с сайд-эффектами;
- счетчик для демонстрации синхронизации глобального состояния;
- кнопка инкремента глобального состояния.
Для демонстрации подходов и легкого понимания отлично подойдут максимально упрощенные примеры. Именно поэтому мною были использованы примеры со счетчиком. В конце статьи будет подведен небольшой итог по выбору того или иного подхода с перечнем ссылок для глубокого погружения. А ссылки на репозитории каждого проекта будут приведены в соответствующем разделе.
Native state
Все, что вы используете во Flutter, состоит из виджетов. Они могут быть видимыми, невидимыми, содержать дочерние виджеты и взаимодействовать между собой. Каждый из них может быть как виджетом без состояния (Stateless Widget), так и виджетом, у которого есть состояние (Stateful Widget). Основное отличие — возможность повторно отрисовывать виджеты во время выполнения приложения. Stateless Widget будет отрисовываться только один раз и является неизменяемым. Stateful Widget может отрисовываться множество раз в зависимости от изменения внутреннего состояния виджета.Для создания Stateful Widget вам нужно создать 2 класса.
Первый класс должен наследоваться от Stateful Widget, который в свою очередь наследуется от Widget и является неизменяемым. Экземпляр этого класса не пересоздается при каждой отрисовке и используется для хранения переданных параметров и инициализации состояния.
Второй — класс состояния, который имеет доступ к Stateful Widget через внутреннее свойство и занимается непосредственно отрисовкой состояния, реагируя на его изменение.
Глобальное состояние приложения, так же как и локальное, может быть реализовано с помощью Stateful Widget. Этот виджет создается в самом верхнем узле приложения и передается вниз по виджетам с помощью InheritedWidget. Каждый дочерний виджет приложения может получить доступ к виджету глобального состояния для изменения и использования его полей.
Для управления состояниями с сайд-эффектами мы руководствуемся теми же принципами. Вызываем асинхронную функцию, которая последовательно устанавливает флаг для индикации о загрузке с сервера, выполняем нужный нам запрос на сервер и меняем состояние в зависимости от пришедших данных.
Преимущества и недостатки
Преимуществами такого подхода является простота и скорость внедрения в приложение с небольшим количеством экранов, которая выражается в отсутствии каких-либо дополнительных библиотек.Из недостатков можно выделить такие:
- Представление и бизнес-логика никак не разделены.
- Сложность в модульном тестировании.
- Сложность в масштабировании и поддержке.
- Большое количество кода, который не может быть переиспользован.
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-расширений на все потребности программиста в управлении состоянием.
Формально у нас существуют такие понятия:
- 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, так что окончательный выбор будет зависеть от функционала, который предусматривается в приложении.
Источник статьи: https://dou.ua/lenta/articles/flutter-architecture/