Перейти к содержанию

Multi-module Architecture

Multi-module architecture разделяет Android-проект на Gradle-модули с явными границами, зависимостями и владельцами. Она становится полезной, когда один application module уже сложно собирать, тестировать, изучать или поддерживать.

Цель не в максимальном количестве модулей. Хорошие модули отражают реальные продуктовые или технические обязанности и предоставляют небольшие стабильные API.

Какие проблемы решает модульность?

Модульность может дать:

  • более понятное владение кодом и строгие границы зависимостей;
  • изолированное тестирование и изменение реализаций;
  • переиспользование между фичами или приложениями;
  • параллельную работу команд с меньшим количеством конфликтов;
  • более быстрые incremental или parallel builds, если это поддерживают граф зависимостей и конфигурация сборки.

Эти преимущества не возникают автоматически. Каждый модуль добавляет Gradle configuration, настройку зависимостей, DI и navigation contracts, а также расходы на поддержку. Избыточное количество плохо связанных модулей может замедлить и разработку, и сборку. Небольшому приложению часто достаточно packages внутри :app или нескольких крупных модулей.

Стоит стремиться к высокой связности кода внутри модуля и низкой связанности между модулями.

Стратегии модульности

Разделение по слоям

При таком подходе архитектурные слои становятся Gradle-модулями:

:app
:presentation
:domain
:data

Направление зависимостей легко объяснить: 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 группирует код по пользовательским возможностям:

:app
:feature:profile
:feature:payments
:feature:settings
:core:network
:core:database
:core:ui

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:* владеет продуктовой возможностью. Внутри можно использовать packages presentation, 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.

Связанные темы