Эта статья - перевод недавно вышедшего гайда о модуляризации Android-приложений от Google.
Начнем с того, что какой-то единой стратегии построения многомодульных приложений, подходящей для всех проектов не существует. Все зависит от задач которые вы решаете и проекта. С помощью системы сборки Gradle вы можете гибко организовать многомодульный проект. Поэтому в этой статье мы рассмотрим общие правила и шаблоны, которые можно использовать при разработке большинства многомодульных приложений для Android.
Принципы высокой сцепленности и слабой связанности
Код, разбитый на модули характеризуется свойствами сцепленности и связанности. Свойство связанности показывает степень зависимости модулей друг от друга. Сцепленность характеризует то, насколько с функциональной точки зрения похожи элементы в одном модуле. Как правило, нужно стремиться к высокой сцепленности и слабой связанности.
Слабая связанность означает, что модули должны быть максимально независимы друг от друга, таким образом изменения в одном модуле не влияют на другие модули. Модули не должны знать о внутренней реализации других модулей.
Высокая сцепленность подразумевает что модули должны содержать компоненты, которые работают как система. У каждого компонента должна быть своя четкая обязанность и домен, предметная область. Рассмотрим приложение для чтения электронных книг. Связывать код, отвечающий за чтение книги и оплату в одном модуле не самая лучшая идея, так как это 2 разных функциональных домена.
Совет: если работа двух модулей зависит от функционала каждого из них, возможно это знак, что они должны работать как единая система в рамках одного модуля. И наоборот, если компоненты внутри одного модуля не взаимодействуют друг с другом, то возможно их следует разделить по разным модулям.
Типы модулей
То, как вы организуете модули, в основном зависит от архитектуры вашего приложения. Ниже приведены некоторые распространенные примеры модулей, которые вы можете добавить в свое приложение, следуя рекомендуемой архитектуре Android-приложений.
Data-модули (модули данных).
Data-модуль обычно содержит репозиторий, источники данных и классы моделей. Три основные обязанности data-модуля:
Инкапсуляция, сокрытие данных и бизнес-логики для конкретной предметной области, домена. Каждый data-модуль отвечает за обработку и работу с данными представляющими конкретный домен, предметную область. Он может обрабатывать многие типы данных, если они связаны.
Предоставить репозиторий в качестве внешнего API. Общедоступный API data-модуля должен быть репозиторием, поскольку он отвечает за предоставление данных остальной части приложения.
Скрыть доступ к деталям реализации и Data Source: Источники данных (Data Source) должны быть доступны только репозиториям из того же модуля. Они остаются скрытыми от доступа снаружи. Вы можете обеспечить это, используя ключевое слово private или internal visibility.
Feature-модули. Функциональные модули.
Под фичей (feature) подразумевается изолированная часть функциональности приложения, которая обычно соответствует экрану или набору тесно связанных экранов, таких как процесс регистрации или оформления заказа. Если в вашем приложении есть Bottom Bar для навигации, вполне вероятно, что каждый пункт назначения является отдельной фичей.
Фичи (feature) связаны с экранами или местами назначения в вашем приложении. Они, скорее всего, будут иметь связанный пользовательский интерфейс и ViewModel для обработки своей логики и состояния. Примером может быть функционал оплаты, авторизация пользователя. Функциональность не обязательно должна быть ограничена одним экраном или местом навигации. Feature-модули зависят от data-модулей.
App-модули. Модули приложения.
App-модули являются точкой входа в приложение. Они зависят от feature-модулей и обычно обеспечивают корневую навигацию. Один модуль приложения может быть скомпилирован в несколько разных двоичных файлов благодаря разным вариантам сборки (build variant).
Если ваше приложение предназначено для нескольких типов устройств, таких как android auto, android wear или android TV, вы можете разделить модули приложения для каждого из устройств. Это помогает отделить зависимости от конкретной платформы.
Common-модули. Общие модули.
Общие модули, также известные как базовые (core) модули, содержат код, который часто используется другими модулями. Они уменьшают избыточность и не представляют какой-либо конкретный уровень в архитектуре приложения. Ниже приведены примеры общих модулей:
UI-модуль. Если у вас есть настраиваемые UI-компоненты или сложные брендинг, вы можете выделить UI-виджеты в отдельный модуль для дальнейшего повторного использования. Это поможет сделать UI вашего приложения консистентным. Кроме того, если ваш UI выделен в отдельный модуль, то вы сможете избежать болезненного рефакторинга при ребрендинге.
Модуль аналитики: отслеживание различных событий часто диктуется бизнес-требованиями без учета архитектуры программного обеспечения. Трекеры аналитики часто используются во многих несвязанных между собой компонентах. Если это ваш случай, было бы неплохо иметь специальный модуль для аналитики.
Сетевой модуль: если многим модулям требуется сетевое подключение, вы можете рассмотреть возможность создания модуля, предназначенного для предоставления http-клиента. Это особенно полезно, когда вашему клиенту требуется индивидуальная конфигурация.
Утилитный, служебный модуль: утилиты, различные хэлперы, обычно представляют собой небольшие фрагменты кода, которые повторно используются в приложении. Примеры утилит включают классы по тестированию, функцию форматирования валюты, валидатор для проверки электронной почты.
Взаимодействие между модулями.
Модули редко существуют полностью отдельно и часто полагаются на другие модули и взаимодействуют с ними. Важно поддерживать низкую связанность, даже когда модули работают вместе и часто обмениваются информацией. Иногда прямая связь между двумя модулями нежелательна из-за ограничений архитектуры или из-за циклических зависимостей.
Чтобы решить эту проблему, можно иметь третий модуль, который является посредником между двумя другими модулями. Модуль-посредник может прослушивать сообщения от обоих модулей и пересылать их по мере необходимости. В нашем примере приложения экран проверки должен знать, какую книгу купить, даже если событие возникло на отдельном экране, который является частью другой функции. В этом случае посредником является модуль, которому принадлежит навигационный граф (обычно это модуль приложения). В этом примере мы используем навигацию для передачи данных из feature-модуля Home в feature-модуль Checkout с помощью компонента навигации.
navController.navigate("checkout/$bookId")
Используя в качестве аргумента id книги можно будет получить полную информацию о книге.
class CheckoutViewModel(savedStateHandle: SavedStateHandle, …) : ViewModel() {
val uiState: StateFlow<CheckoutUiState> =
savedStateHandle.getStateFlow<String>("bookId", "").map { bookId ->
// produce UI state calling bookRepository.getBook(bookId)
}
…
}
Вы не должны передавать объекты в качестве аргументов для навигации. Вместо этого используйте простые идентификаторы, через которые можно загрузить данные используя data-слой. Таким образом, вы поддерживаете низкую связанность и не нарушаете принцип единственного источника истины (single source of truth principle).
В приведенном ниже примере оба feature-модуля зависят от одного и того же модуля данных (data:books). Это позволяет свести к минимуму объем данных, которые модуль-посредник должен пересылать, и поддерживает низкую связь между модулями. Вместо передачи объектов модули должны обмениваться идентификаторами примитивов и загружать ресурсы из общего data-модуля.
Общие рекомендации
Как упоминалось в начале, не существует единственно правильного способа разработки многомодульного приложения. Точно так же, как существует множество программных архитектур, существует множество способов разбиения приложения на модули. Тем не менее, следующие общие рекомендации помогут вам сделать код более читабельным, удобным для сопровождения и тестирования.
Поддерживайте консистентную конфигурацию
Каждый модуль вводит дополнительную нагрузку на настройку конфигурации модуля. Если количество ваших модулей достигает определенного порога, управление конфигурацией становится проблемой. Например, важно, чтобы модули использовали зависимости от библиотек одной и той же версии. Если вам нужно обновить большое количество модулей только для того, чтобы поднять версию библиотеки, это не только дополнительное время и усилие, но и место для потенциальных ошибок. Чтобы решить эту проблему, вы можете использовать один из инструментов Gradle для единой конфигурации:
Каталоги версий — это безопасный список зависимостей, генерируемый Gradle во время синхронизации. Это единое место для объявления всех ваших зависимостей, доступное для всех модулей проекта.
Совместное использование скриптов сборки между подпроектами
Пример описания версий всех библиотек в одном месте и дальнейшее использование
Свести к минимуму публичный API
Публичный интерфейс модуля должен быть минимальным и отображать только самое необходимое. Детали реализации не должны быть доступны наружу. Используйте модификаторы private
или internal
для уменьшения области видимости, чтобы сделать методы или свойства приватными для модуля. При объявлении зависимостей в вашем модуле отдавайте предпочтение подключению через implementation
а не api
. Последний предоставляет транзитивные зависимости потребителям вашего модуля. Использование implementation
может сократить время сборки, поскольку уменьшает количество модулей, которые необходимо пересобрать после изменений.
Предпочитайте Kotlin и Java-модули вместо Android-модулей.
Android Studio поддерживает три основных типа модулей:
App-модули, модули приложения — это точка входа в ваше приложение. Они могут содержать исходный код, ресурсы, Activity и файл AndroidManifest.xml. Результатом модуля приложения является артефакт приложения Android (AAB или APK).
Модули библиотеки имеют то же содержимое, что и App-модули. Они используются другими модулями Android в качестве зависимости. Выходные данные библиотечного модуля — это Android-архив (AAR), структурно идентичные модулям приложения, но они скомпилированы в AAR, который впоследствии может использоваться другими модулями в качестве зависимости. Модуль библиотеки позволяет инкапсулировать и повторно использовать одну и ту же логику и ресурсы во многих App-модулях.
Kotlin и Java-модули не содержат никаких ресурсов Android, Activity или файлов Android-манифеста. Если в вашем модуле нет зависимости от Android SDK, желательно, чтобы вы использовали Kotlin или Java - модуль.
Не забудьте присоединиться к нам в Telegram, а на платформе AndroidSchool.ru[ссылка удалена модератором] публикуются полезные материалы для Android-разработчика и современные туториалы.
Комментарии (11)
snuk182
13.09.2022 00:06+1Странно, что модулями здесь называется либо общий код, либо разные реализации нужного интерфейса. Во всем мире это обычные библиотеки. Модуль же - нечто, что можно воткнуть-вынуть на лету без пересборки.
android_school_ru Автор
13.09.2022 00:29В основном, когда говорим о модулях в контексте android-разработки, то имеются ввиду Android-модули. Но иногда, если не нужны классы из Android SDK, то достаточно java или kotlin - библиотеки, но в оригинальной статье их тоже называют модулями.
WraithOW
13.09.2022 12:41Потому что в терминах Gradle и Maven, которые де-факто стандарты для Java/Kotlin разработки, подпроекты называются модулями. Библиотека - внешняя зависимость, модуль - обособленная часть проекта.
Во всем мире это обычные библиотеки
snuk182
13.09.2022 14:09По ссылке — реф на модули в плюсах, принятые только в стандарте С++20.
В сборочных тулзах Java подмодулями называются именно подпроекты этих тулзов, но каждый из них собирается в библиотеку, в случае ванильной Java — в shared, в случае ведра — в AAR. В ведре действительно есть «модули», отвечающие просто за кусок расшаренного кода — это как раз то, что я и назвал странным. Впрочем, это терминология самого гугла.
jershell
Жаль перевод. Есть несколько вопросов и дополнений. Часто вижу как делить, но ни где не видел зачем делить. Хоть бы один какделитель написал бы, что это нужно, когда у вас instant или вы шарите между несколькими разными приложениями какой-либо функционал, в остальных случаях деление почти всегда избыточно, а пакетов хватает с лихвой. Так же что есть и цена этого деления, поддержка клея между модулями тоже требует и время и увеличивает сложность проекта. Чем больше модулей, тем больше клея, тем дороже эта модульность обходится.Без явной практической необходимости не стоит этого делать.
android_school_ru Автор
Современные android-приложения уже давно переваливают за несколько сотен экранов. Таким образом деление на модули - способ организации кода для дальнейшего переиспользования и уменьшения связанности даже в пределах одного приложения. В статье есть примеры data-модулей, утилитных модулей - они могут пригодиться даже в небольшом проекте. А если у вас команда в проекте больше 20 android-разработчиков, то выделение в отдельные фича -модули позволяют ускорить разработку, уменьшить кол-во merge-конфликтов, снизить кол-во ошибок во всем приложении. Не зря большие команды типа Uber имеют несколько десятков, а то и сотен модулей. Если у вас небольшое приложение с небольшой командой - то скорее всего разбиение на модули и не нужно.
ChPr
В последнее время многомодульность преподносится в виде решения вообще всех насущных проблем.
Мердж конфликты появляются когда несколько человек трогают одно и то же место, нет никакой разницы, будет это место в одном из сотен модулей или в одном единственном. Если экраны раскидать по пакетам как по модулям, а не городить package by type, то и конфликтов будет ровно столько же.
Как именно многомодульность уменьшает количество ошибок в приложении мне тоже не очень ясно.
И все это идет с огромной пачкой проблем по поддержанию корректности структуры модулей и поддержки Gradle конфигураций.
WraithOW
Пока ваше приложение умещается в десяток-другой экранов - да. А потом наступает момент, когда вы начинаете тратить по 5-10-15 минут на сборку, потому что у вас мономодуль и фишки типа параллельной сборки и кеширования вам недоступны.
jershell
А есть цифры во сколько раз ускоряется сборка? После какого числа пакетов модульная сборка начинает обгонять обычную сборку? Почему параллельная сборка не работает в проекте "на пакетах"?
WraithOW
От проекта к проекту отличается, надо мерять. На моем прошлом жирном проекте вроде около половины смогли срезать.
Та же история. В теории - с любого, хотя бы за счёт более агрессивного кеширования.
Работает, но сильно ограниченно. Шаги сборки нельзя выполнять параллельно: сначала препроцессинг, потом компиляция, потом дексинг, потом упаковка. Какие-то шаги типа дексинга плохо параллелятся внутри, если у вас есть такой шаг - он становится бутылочным горлышком. Компилятор может параллелить работу, но ему всё равно сначала нужно переварить весь код модуля чтобы понять, что поменялось, что нужно пересобрать, и что из этого можно распараллелить.
Если же у вас есть модули, то у системы сборки есть простые и четкие критерии того, что можно гарантированно параллелить и кешировать, а что нужно пересобрать. Если два модуля не зависят друг от друга - их можно параллелить. Если в модуле нет изменений и ни одна из его зависимостей не поменялась - модуль не нужно пересобирать, даже смотреть на него не нужно.
Можете сами прикинуть, что проще обработать: граф модулей из 30 нод или граф зависимостей классов из нескольких тысяч нод.
Paul85
вот точно так же думаю. Я пришел к тому, что многомодульный проект будет оправдан если на проекте несколько команд или если какие-то фичи динамические и не во все билды нужны.