Не так давно вышла статья о чистой архитектуре во Flutter. Хочу осветить тему немного под другим углом и развить тему управления глобальным состоянием с помощью Redux.
И немного о себе: я занимаюсь созданием коммерческих продуктов около 10 лет, из которых на Flutter почти 2 года и успел попробовать все известные стейт-менеджеры. Какие-то вызывают нейтральные воспоминания - BLoC, Provider, глобальный класс-блок со своими стримами, а какие-то и негативные - MobX.
В итоге для себя я остановился на Redux для глобального состояния и библиотек для реализации структуры приложения:
built_value
built_collection
rxdart (по необходимости)
flutter_simple_dependency_injection (или dioc)
built_redux
Это мой минимальный набор библиотек для реализации проектов любого уровня.
А теперь по шагам
Общая структура приложения
Общая структура приложения
Папки в корне все стандартные, создаются автоматически, но есть дополнительные:
go - папка для запуска супер облегченной десктоп версии приложения с помощью hover. Об этом уже была моя статья. Все платформонезависимые плагины работают, недостающие пишутся на GoLang. Плюсы, помимо того, что ресурсы компьютера тратятся по минимуму и возможности быстрого просмотра верстки под различные размеры, отмечу возможность просмотра локальной базы sqlite без лишних телодвижений, если таковая используется. Просто открываем файл любимой IDE, например, SQLiteStudio. Создается стандартно -
hover init
, получаем готовое десктоп-приложение
build.yaml - для генератора built_value, будем создавать с помощью него модельки
analysis_options.yaml - файл для настроек линтера. Мастхев.
scripts - здесь лежат различные скрипты для запуска эмуляторов/десктоп/ховер/веб версий или вспомогательных операций: запуск генерации файлов, форматирование кода, создание локальных настроек, проверка версии Flutter, чтобы вся команда работала на одной версии. Часть команд запускается из пре-тасок. Пример: prepare_app - проверка версии Flutter, prepare_app_hover - проверка версии и запуск самого hover. Так же есть файлы докера для запуска сервера центрифуги, который будет помогать отладку приложения (немного про отладку приложения здесь) и скрипт для запуска dartfix - очень помогает справиться с легаси кодом
application_bundle - папка с настройками. Например, здесь можно расположить различные JSON файлы с настройками под различные раннеры - сборки в зависимости от назначения приложения - "лайт" версия приложения, с урезанным функционалом, полная версия и т.д.
Пример пре-тасок
Пример скрипта
Структура Flutter-приложения
В самой папке lib я обычно создаю следующие файлы и папки:
domain - здесь хранится само ядро приложения: наборы экшенов, базовые классы, эпики, миддлвары, модельки, редьюсеры, селекторы и классы состояния
tools - различные утилиты
di - класс, реализующий инверсию зависимостей
features - UI часть, например, реализация страницы пользователя
services - различные сервисы, например, сервис для сохранения локальных настроек, сервис логгера
app - базовые классы для запуска приложения. Здесь я располагаю MaterialApp или CupertinoApp
app_routes.dart - класс с названиями роутов для навигации
Domain
models/enums
раньше я создавал модели в ручном или “полуавтоматическом” режиме с помощью генератора моделей, например, quicktype. Удобно, легко позволяет сгенерировать модельки из JSON файла с сериализацией/десериализацией, но т.к. нет иммутабельности и возможности проставить начальные значения при десериализации, отказался от этого подхода в пользу генератора built_value, у которого есть все необходимое:
Пример реализации класса-модели
actions
созданные built_redux генератором модели экшенов для быстрого старта Redux-приложения
Пример реализации класса-модели экшена
middlewares
набор миддлваров, собирается в единое приложение так же библиотекой built_redux
Пример реализации класса-миддлвара
epics
набор эпиков для асинхронной обработки экшенов. Так же built_redux. В основе обработки стандартные стримы и тут при желании и необходимости можно применить библиотеку rxdart
Пример реализации класса-эпика
reducers
набор редьюсеров, меняющих наш глобальный стейт
Пример реализации класса-редьюсера
states
набор состояний Redux. Здесь есть один общий класс-состояние - AppState, хранящий в себе другие состояния: состояние пользователя, состояние локальных настроек и т.д. Собирается так же built_redux
Пример реализации класса-состояния
DI
Инверсия зависимостей для различных сервисов. Для быстрого старта подойдет flutter_simple_dependency_injection
Пример реализации инверсии зависимостей
Features
Фичи - наборы папок, зависящих от домена и ничего не знающие про соседние фичи. Состоят из нескольких папок - blocs, components, widgets, tools. Точка входа - как правило один виджет в папке widgets по названию папки. Каждый виджет зависит от своего блока(BLoC) - обычный класс, который стримит данные в этот виджет. Главный принцип - один блок - один виджет. Жизненный цикл которого для StatefulWidget’а
создание в initState
работа - стриминг в StreamBuilder’ов
уничтожение в dispose
При необходимости создаются дополнительные компоненты в папке components - более мелкие виджеты необходимые для отрисовки виджета-фичи, которые так же могут быть с такой же структурой папок
BLoC
Класс с бизнес-логикой для виджета-фичи. Наследуется от базового абстрактного класса, который имеет доступ к домену и di-контейнеру. Содержит методы, поля для наследуемых классов-блоков
Пример базового абстрактного класса BLoC
Классы-блоки виджетов наследуются от базового класса BaseBloc и стримят нужные состояния в виджеты. Я создаю вручную сабжекты и нужные стримы
Пример BLoC-класса
Здесь:
объявление сабжектов и стримов
закрытие подписок, сабжектов
инициализация класса. Создание сабжектов и подписка/подписки на изменения нужных состояний, например, профиля. Слушаем изменение, делаем какие-то манипуляции (фильтрация, дебаунс, троттл, и т.д.), добавляем в сабжект (либо же, конечно, если не нужны дополнительные манипуляции со стейтом, то стримим сразу nextSubstate)
И дополнительные методы для какой-то логики изменения состояния виджета либо для изменения домена путем вызова экшенов
Пример метода для вызова экшена
Виджеты
Отрисовываем изменение состояния нужного стейта с помощью StreamBuilder:
Отрисовка изменения состояния
Заключение
Описанный мной подход, конечно же, как и все остальные подходы по организации структуры кода своего приложения имеет как плюсы, так и минусы.
Минусы:
трата времени на генерацию файлов. Если требуется внести правки в один файл, перегенерятся и другие. Для 300 классов время может занимать до 2х минут
многословность Redux и, как правило, много бойлерплейт
Плюсы:
иммутабельность, сериализация, дефолтные значения с built_value “из коробки”
быстрое обучение новичков из мира фронтенда знающих Redux
почти безболезненное включение/выключение фич
независимая работа членов команды над задачами. Например, на моей текущей работе в команде 13 Flutter разработчиков, абсолютно не мешающих друг другу благодаря вышеописанной структуре приложения
доменная часть, независимая от UI - как пример чистой архитектуры, что дает возможность поместить ее (целиком Redux состояние со всеми миддлварами и эпиками, выполняющими много работы в бекграунде) в свой отдельный изолят
И хочу добавить, что простых проектов не бывает. Бывает и пет-проект разрастается до коммерческих продуктов. И тогда после неправильно спроектированной архитектуры нормальная работа может стать невозможной. У меня есть проект, так же на Flutter, который я ради интереса попробовал написать через архитектуру MobX. Проект разросся. Работать стало, мягко говоря, некомфортно, пришлось все переписывать на Redux.
Целью этой статьи было обратить внимание начинающих или опытных разработчиков на Redux архитектуру, которая хорошо зарекомендовала себя на очень большом коммерческом продукте с десятками тысяч ежедневных пользователей. Пережила и выдержала приходы/уходы коллег, внедрение/удаление различных фичей.
MaZaAa
Через архитектуру MobX? Нет такого понятия.
MobX лишь дает настоящую реактивность, а как именно организовывать архитектуру, это уже сугубо личное дело каждого разработчика.
Так вот, вы просто сделали плохую архитектуру. Причем тут MobX???