Multi-module Architecture
Multi-module architecture разделяет Android-проект на Gradle-модули с явными границами, зависимостями и владельцами. Она становится полезной, когда один application module уже сложно собирать, тестировать, изучать или поддерживать.
Цель не в максимальном количестве модулей. Хорошие модули отражают реальные продуктовые или технические обязанности и предоставляют небольшие стабильные API.
Какие проблемы решает модульность?
Модульность может дать:
- более понятное владение кодом и строгие границы зависимостей;
- изолированное тестирование и изменение реализаций;
- переиспользование между фичами или приложениями;
- параллельную работу команд с меньшим количеством конфликтов;
- более быстрые incremental или parallel builds, если это поддерживают граф зависимостей и конфигурация сборки.
Эти преимущества не возникают автоматически. Каждый модуль добавляет Gradle configuration, настройку зависимостей, DI и navigation contracts, а также расходы на поддержку. Избыточное количество плохо связанных модулей может замедлить и разработку, и сборку. Небольшому приложению часто достаточно packages внутри :app или нескольких крупных модулей.
Стоит стремиться к высокой связности кода внутри модуля и низкой связанности между модулями.
Стратегии модульности
Разделение по слоям
При таком подходе архитектурные слои становятся Gradle-модулями:
Направление зависимостей легко объяснить: presentation использует domain contracts, data предоставляет реализации repositories, а :app собирает граф.
Недостаток в том, что одно продуктовое изменение может затронуть все технические модули:
:presentation -> payment screen, ViewModel, UI state
:domain -> payment use case и repository contract
:data -> repository implementation, API и cache
Такой вариант может работать в небольших и средних проектах, но широкие layer-модули часто превращаются в технические монолиты с неясным владением фичами.
Разделение по фичам
Feature-based modularization группирует код по пользовательским возможностям:
Feature module может владеть своим UI, состоянием, специфичной для фичи логикой и точками входа навигации. Это обычно соответствует тому, как продуктовые команды выпускают изменения, и упрощает изоляцию, замену или удаление фичи.
При этом feature module тоже может стать мини-монолитом. Общий код может дублироваться или слишком рано выноситься наружу, а навигации между фичами нужны явные контракты.
Прагматичный гибридный подход
Распространённая практическая структура сочетает feature modules со сфокусированной инфраструктурой и общими бизнес-модулями:
:app
:core:network
:core:database
:core:designsystem
:core:analytics
:shared:user
:shared:subscriptions
:feature:profile
:feature:payments
:feature:settings
Эти названия являются соглашениями, а не требованиями Android:
:app- точка входа приложения. Он отвечает за startup, root navigation и сборку приложения, но не должен содержать всю продуктовую логику.:core:*содержит сфокусированные технические возможности: networking, persistence, analytics или design system. Не стоит создавать общийcore:utils, куда попадают несвязанные helpers.:shared:*содержит стабильные бизнес-возможности, которые действительно используются несколькими фичами. Код одной фичи обычно должен оставаться внутри неё.:feature:*владеет продуктовой возможностью. Внутри можно использовать packagespresentation,domainиdata, не превращая каждый package в отдельный Gradle-модуль.
Делить крупную фичу дальше на api и impl либо на presentation/domain/data modules стоит только ради переиспользования, независимого владения, контроля зависимостей или производительности сборки.
API модулей и правила зависимостей
Наружу следует открывать как можно меньше. Kotlin-объявления с internal остаются внутри Gradle-модуля и не дают другим модулям зависеть от деталей реализации. В public API должны входить только стабильные точки входа, модели и контракты.
Для Gradle-зависимостей:
implementation(project(...))не передаёт dependency потребителям и должна быть вариантом по умолчанию;api(project(...))делает dependency доступной потребителям и нужна только тогда, когда та является частью public API модуля.
Простой граф по умолчанию:
feature -> core
feature -> shared
feature -> feature // обычно избегаем
app -> features // сборка приложения
Прямые зависимости между feature modules создают скрытую связанность и могут привести к циклам. Лучше выделить небольшой общий contract или api module, а связывание implementations оставить :app или root navigation.
Например, :feature:profile не должна зависеть от реализации payments только ради открытия экрана оплаты. Она может использовать или отправить navigation contract, а coordinator уровня приложения определит destination.
Если возникает цикл зависимостей, стоит пересмотреть границы: выделить меньший общий contract, инвертировать зависимость через interface или перенести orchestration выше.
Как принять решение
Создавайте Gradle-модуль, когда выполняются несколько условий:
- у кода есть цельная ответственность и стабильная граница;
- он переиспользуется несколькими фичами или приложениями;
- команда может владеть им независимо;
- модуль предотвращает нежелательные зависимости;
- его можно собирать и тестировать изолированно;
- измерения показывают пользу для isolation или build cache;
- модуль предоставляет небольшой и осмысленный public API.
Оставьте код в существующем модуле, если новый модуль только повторяет папку, имеет нестабильную границу, нужен одной небольшой фиче или заставляет обычные изменения затрагивать больше модулей без дополнительной изоляции.
Начинайте с самой простой подходящей структуры. Выделяйте модули в ответ на реальные проблемы владения, переиспользования, зависимостей или измеренную производительность сборки, а не ради заданного количества.
Конфигурация сборки
Модули могут улучшить incremental compilation, parallel execution и повторное использование build cache, потому что неизменившиеся части сборки можно пропустить. Одновременно они увеличивают configuration и task overhead. Измеряйте clean build и типичные incremental changes до и после модульности, а не предполагайте ускорение заранее.
По мере роста проекта используйте convention plugins, version catalogs и единые module templates, чтобы не копировать Gradle configuration в каждый build.gradle.kts.