В последнее время Flutter набрал популярность для разработки кросс-платформенных приложений. Чаще всего его рассматривают не только как на инструмент, который позволяет закрыть проблемы мобильных платформ, но и как web и десктоп решение на единой кодовой базе и едином процессе разработки.
У этой популярности, конечно, множество причин и, кроме очевидных, немаловажным, как мне кажется, является то, что Flutter+Dart - это современное и изящное средство для разработки.
Под “изяществом” я понимаю то, что Dart язык и платформа Flutter инструменты современные, не тянут за собой легаси, стараясь использовать современные паттерны решения типовых задач. При этом, на мой взгляд, разработчики платформы стараются не усложнять вещи там, где это не нужно или нужно очень редко (примером тому видится отсутствие настоящей мультизадачности, например). Кроме того, команде разрабатывающей flutter, удалось действительно постепенно превратить фреймфорк в реальное средство кросс-платформенной разработки, позволяющей реально работать принцип “Написал единожды - запускаешь на множестве платформ”
В итоге, всё чаще и чаще разные команды начинают присматриваться и отдавать предпочтения flutter-фреймворку для решения задач построения все более сложных систем.
А поскольку при построении систем “Enterprise” уровня во главу угла ставятся несколько другие приоритеты, чем при разработке небольших систем, то встают ребром вопросы, как использовать Flutter Framework эффективно, при этом не теряя его сильных черт, его изящества и простоты.
В этой статье я хотел бы описать небольшой шаблон репозитория, который я бы сам использовал, если бы организовывал разработку средней сложности flutter - приложения посредством нескольких параллельных небольших команд, ответственных за свои части общей клиентской системы.
По большому счету данный пример является развитием идей, изложенных в этой статье и взятых за основу при разработке мобильной части проекта Stages ребятами из этой команды, с которыми мне повезло поработать, а также идей, описанных в этой статье
[1] Прежде всего надо поставить и ответить на примерно такой список вопросов:
Architecture and project structure, code ownership in the team Как структура кода будет отражать организационную структуру и процессы компании, как распределены ответственности за различные модули, с помощью какой архитектуры обеспечивается эффективность разработки, поддержки, масштабирования кода?
CI/CD Каковы правила организации непрерывного процесса, начинающегося с анализа требований, далее к постановке задач, затем разработку, тестирование, доставку до пользователя, анализ метрик, и дальнейших принятий решений.
Flavoring (multiple environments) Каковы принципы организации множественных окружений в которых разрабатывается, тестируется и эксплуатируется софт. Для каждого типа свои требования и особенности, как это интегрировать в код простым и прозрачным способом.
App distribution to testers & stakeholders, App distribution to clients Как построен процесс первичного тестирования, презентации релизов для внутреннего пользования и доставки в продакшн. Как это сделать максимально просто, унифицировано и прозрачно для всех.
Analytics & performance monitoring Как мы формулируем, собираем и анализируем метрики?
Monitoring/logging Как мы мониторим работоспособность системы в целом и как мы разбираемся с конкретными инцидентами.
Design system Очень важно иметь единую понятную дизайн систему, набор примитивов и их иерархию, процессы тестирования сделанной верстки, единый Storybook и тп. Например, Stages: Design system.
Manual and auto testing Как мы тестируем новые фичи, как мы тестируем работоспособность старых, что делается вручную, что автоматизировано. Какой парк устройств и платформ используется, как это документируется и логируется.
Localization & internationalization Во flutter из коробки присутствует простая и понятная система локализации/интернационализации. Но многие находят, что удобнее пользоваться надстройками над этой базовой системой (например https://pub.dev/packages/multilizely) в виде интеграции со сторонними системами централизованной локализации, например https://localizely.com/, позволяющей превратить локализацию в постоянный контролируемый процесс. Также я видел несколько примеров, когда команды писали свои системы на основе google таблиц.
Navigation (with deeplink support) Лучше сразу определиться с подходами к системе навигации, ее интеграцией с модулями (внутренняя, внешняя). Мне кажется разумным абстрагировать эту систему от конкретных реализаций типа go_router или beamer, что можно сделать похожим образом, как мы абстрагируем data слой в чистой архитектуре.
Team Code Style && Code Commenting Guidelines
Version control workflow
-
New-comers onboarding
Поговорим подробнее про первую задачу.
1 Architecture & project structure, modularisation, module-to-module interoperating, code ownership in the team
Взятая отсюда первая идея, которая управляет процессом структурирования кода состоит в том, что ответственность за части проекта должны лежать на командах, а не отдельных людях и хорошо, когда структурирование кода отражает структуру и процессы коммуникации между командами, отвечающими за разные бизнес сущности.
Те, хотя разрабатывать код в рамках единого монолитного репозитория, на первый взгляд, проще и быстрее, очень быстро бойлерплейт, которым мы платим за структурирование кода на модули/пакеты, взаимодействия через интерфейсы и тому подобное, станет меньшим злом, чем тот хаос, в который превратится разработка иначе. Обособить части кода, отвечающие за разные бизнес-процессы, выполняемые разными подразделениями даст много преимуществ через некоторое время. Назовем это вертикальным разбиением.
Вторая идея - это классическая чистая архитектура, когда в рамках каждого модуля илу зависимости от дяди Боба. Стараемся по возможности, каждый раз проектируя класс, задаваться вопросом - а на каком уровне нашей горизонтальной иерархии он живет?
Может это доменная сущность, отвечающая за базовую бизнес логику нашего модуля(бизнес-домена)? Тогда не стесняемся прямо положить его в поддерево domain нашего модуля и, по можно даже сделать правилом(в дополнение к правилу “кричащей архитектуры”.) называть эти классы Entity (UserEntity, TripEntity). И пишем его на чистом Dart с минимальным или вообще нулевым количеством внешних зависимостей.
Для Presentation или UI слоя мы любим пользоваться тут BLoC паттерном для написания “бизнес-логики компонентов”, по сути контроллеров визуальных виджетов. Удобное средство отделения “мух от котлет”. Хорошо ведь, когда возвращаешься к своему коду, написанному год назад - открываешь states.dart, потом events.dart и уже понятно, что там будет в bloc.dart и примерно ясно, как будет отображать это widget.dart . То же справедливо, когда это вообще чужой код и ты видишь его впервые. И вообще тот человек уже уволился. Однако все понятно и понятно, что и как делать.
Если это больше похоже на объект, который реализует взаимодействие с конкретным фреймворком или API, завязан на конкретного поставщика, то ему место в data слое.
Этот слой может быть внутри модуля фичи, если пока не планируем его отдельно использовать за рамками модуля. Если сразу хотим, то выделяем сразу в отдельный модуль, содержащий только data слой и инъекцию этой зависимости делаем в основном в модуле приложения или где-то еще.
Кроме того, такой подход дает возможность работать над разными слоями в разное время людьми разной квалификации/специализации, это удобно тестировать.
Посмотрим на наш пример-паттерн и посмотрим, что принцип “кричащей архитектуры” может о нем рассказать.
У нас монорепозиторий, управляемый melos, в котором
два приложения app1(1) и app2(2)
app1 приложение, которое собирает свой функционал из других модулей/пакетов(features) и содержит минимум функций - платформенные вещи (ios,android,web,macosx), точку входа, конфигуратор из окружения/DI и сам сборщик зависимостей.
видим, что есть три большие фичи (модуля) - это авторизация (3), inbox(4), nofitications(6).
фича app1_main_screen(5) - выделена в отдельную фичу для “чистоты” app1, но может быть перенесена внутрь
feature_auth построена по принципу чистой архитектуры и содержит слои domain (7) и ui (8)
выделен в отдельный модуль auth_provider1 (9), который тоже построен по принципу чистой архитектуры и содержит реализацию авторизации фичи 3 через конкретного поставщика (например, firebase). Таким образом, если для app2, например, нужна будет реализация авторизации через другого поставщика, то весь код, экраны и тп feature_auth использоваться могут, а auth_provider1, а вместе с ним и firebase даже не будет включаться в зависимости и не участвовать в сборке.
вспомогательные пакеты 10 для решения остальных задач изначального списка [1]
Отдельный большой вопрос, это как модули вида features должны/могут зависеть от других features. В идеале никак, но по факту, жизнь сложнее и это тема отдельного обсуждения.
Код можно посмотреть тут - GitHub: clean_app_pattern