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

И немного о себе: я занимаюсь созданием коммерческих продуктов около 10 лет, из которых на Flutter почти 2 года и успел попробовать все известные стейт-менеджеры. Какие-то вызывают нейтральные воспоминания - BLoC, Provider, глобальный класс-блок со своими стримами, а какие-то и негативные - MobX.

В итоге для себя я остановился на Redux для глобального состояния и библиотек для реализации структуры приложения:

  • built_value

  • built_collection

  • rxdart (по необходимости)

  • flutter_simple_dependency_injection (или dioc)

  • built_redux

Это мой минимальный набор библиотек для реализации проектов любого уровня.

А теперь по шагам

Общая структура приложения

Общая структура приложения
Рис. 1. Общая структура приложения
Рис. 1. Общая структура приложения

  Папки в корне все стандартные, создаются автоматически, но есть дополнительные:

  • 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 файлы с настройками под различные раннеры - сборки в зависимости от назначения приложения - "лайт" версия приложения, с урезанным функционалом, полная версия и т.д.

Пример пре-тасок
Рис. 2. Пример пре-тасок
Рис. 2. Пример пре-тасок

Пример скрипта
Рис. 3. Пример из набора скриптов
Рис. 3. Пример из набора скриптов

Структура Flutter-приложения

В самой папке lib я обычно создаю следующие файлы и папки:

  • domain - здесь хранится само ядро приложения: наборы экшенов, базовые классы, эпики, миддлвары, модельки, редьюсеры, селекторы и классы состояния

  • tools - различные утилиты

  • di - класс, реализующий инверсию зависимостей

  • features - UI часть, например, реализация страницы пользователя

  • services - различные сервисы, например, сервис для сохранения локальных настроек, сервис логгера

  • app - базовые классы для запуска приложения. Здесь я располагаю MaterialApp или CupertinoApp

  • app_routes.dart - класс с названиями роутов для навигации

Domain

  • models/enums

    раньше я создавал модели в ручном или “полуавтоматическом” режиме с помощью генератора моделей, например, quicktype. Удобно, легко позволяет сгенерировать модельки из JSON файла с сериализацией/десериализацией, но т.к. нет иммутабельности и возможности проставить начальные значения при десериализации, отказался от этого подхода в пользу генератора built_value, у которого есть все необходимое:

Пример реализации класса-модели
Рис. 4. Пример реализации класса-модели
Рис. 4. Пример реализации класса-модели

  • actions

    созданные built_redux генератором модели экшенов для быстрого старта Redux-приложения

Пример реализации класса-модели экшена
Рис. 5. Пример реализации класса-модели экшена
Рис. 5. Пример реализации класса-модели экшена

  • middlewares

набор миддлваров, собирается в единое приложение так же библиотекой built_redux

Пример реализации класса-миддлвара
Рис. 6. Пример реализации класса-миддлвара
Рис. 6. Пример реализации класса-миддлвара

  • epics

    набор эпиков для асинхронной обработки экшенов. Так же built_redux. В основе обработки стандартные стримы и тут при желании и необходимости можно применить библиотеку rxdart

Пример реализации класса-эпика
Рис. 7. Пример реализации класса-эпика
Рис. 7. Пример реализации класса-эпика

  • reducers

набор редьюсеров, меняющих наш глобальный стейт

Пример реализации класса-редьюсера
Рис. 8. Пример реализации класса-редьюсера
Рис. 8. Пример реализации класса-редьюсера

  • states

    набор состояний Redux. Здесь есть один общий класс-состояние - AppState, хранящий в себе другие состояния: состояние пользователя, состояние локальных настроек и т.д. Собирается так же built_redux

Пример реализации класса-состояния
Рис. 9. Пример реализации класса-состояния
Рис. 9. Пример реализации класса-состояния

DI

Инверсия зависимостей для различных сервисов. Для быстрого старта подойдет flutter_simple_dependency_injection

Пример реализации инверсии зависимостей
Рис. 10. Пример реализации инверсии зависимостей
Рис. 10. Пример реализации инверсии зависимостей

Features

Фичи - наборы папок, зависящих от домена и ничего не знающие про соседние фичи. Состоят из нескольких папок - blocs, components, widgets, tools. Точка входа - как правило один виджет в папке widgets по названию папки. Каждый виджет зависит от своего блока(BLoC) - обычный класс, который стримит данные в этот виджет. Главный принцип - один блок - один виджет. Жизненный цикл которого для StatefulWidget’а

  • создание в initState

  • работа - стриминг в StreamBuilder’ов

  • уничтожение в dispose

При необходимости создаются дополнительные компоненты в папке components - более мелкие виджеты необходимые для отрисовки виджета-фичи, которые так же могут быть с такой же структурой папок

BLoC

Класс с бизнес-логикой для виджета-фичи. Наследуется от базового абстрактного класса, который имеет доступ к домену и di-контейнеру. Содержит методы, поля для наследуемых классов-блоков

Пример базового абстрактного класса BLoC
Рис. 11. Пример базового абстрактого класса BLoC
Рис. 11. Пример базового абстрактого класса BLoC

Классы-блоки виджетов наследуются от базового класса BaseBloc и стримят нужные состояния в виджеты. Я создаю вручную сабжекты и нужные стримы

Пример BLoC-класса
Рис. 12. Пример BLoC-класса
Рис. 12. Пример BLoC-класса

  Здесь:

  1. объявление сабжектов и стримов

  2. закрытие подписок, сабжектов

  3. инициализация класса. Создание сабжектов и подписка/подписки на изменения нужных состояний, например, профиля. Слушаем изменение, делаем какие-то манипуляции (фильтрация, дебаунс, троттл, и т.д.), добавляем в сабжект (либо же, конечно, если не нужны дополнительные манипуляции со стейтом, то стримим сразу nextSubstate)

И дополнительные методы для какой-то логики изменения состояния виджета либо для изменения домена путем вызова экшенов

Пример метода для вызова экшена
Рис. 13. Пример метода для вызова экшена
Рис. 13. Пример метода для вызова экшена

Виджеты

Отрисовываем изменение состояния нужного стейта с помощью StreamBuilder:

Отрисовка изменения состояния
Рис. 14. Отрисовка изменения состояния
Рис. 14. Отрисовка изменения состояния

Заключение

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

Минусы:

  1. трата времени на генерацию файлов. Если требуется внести правки в один файл, перегенерятся и другие. Для 300 классов время может занимать до 2х минут

  2. многословность Redux и, как правило, много бойлерплейт

Плюсы:

  1. иммутабельность, сериализация, дефолтные значения с built_value “из коробки”

  2. быстрое обучение новичков из мира фронтенда знающих Redux

  3. почти безболезненное включение/выключение фич

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

  5. доменная часть, независимая от UI - как пример чистой архитектуры, что дает возможность поместить ее (целиком Redux состояние со всеми миддлварами и эпиками, выполняющими много работы в бекграунде) в свой отдельный изолят

И хочу добавить, что простых проектов не бывает. Бывает и пет-проект разрастается до коммерческих продуктов. И тогда после неправильно спроектированной архитектуры нормальная работа может стать невозможной. У меня есть проект, так же на Flutter, который я ради интереса попробовал написать через архитектуру MobX. Проект разросся. Работать стало, мягко говоря, некомфортно, пришлось все переписывать на Redux.  

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