Создаём многомодульную библиотеку на Android: как же собрать fat-aar?

от автора

В Android-разработке могут возникать сценарии, когда нам нужно собрать один aar из нескольких модулей. 

Однажды нам в Сравни потребовалось создать SDK для наших партнёров — на основе уже существующего проекта. Сделать это хотелось без радикальных изменений в проекте и излишнего раскрытия деталей его устройства.

Задача понятная, но нетривиальная в реализации. Google до сих пор не предоставляет полноценного инструмента для создания fat-aar; к opensource-решениям также много вопросов. 

Выход из ситуации: вникнуть в нюансы того, как работает gradle, и на базе общедоступных средств сделать свой инструмент для создания многомодульной библиотеки. С автоматизированной сборкой модулей и публикацией артефакта. 

О том, как мы к этому подступились и к чему пришли, рассказываем под катом.


Привет, Хабр! Меня зовут Родион, я Android-разработчик в Сравни. В компании мы работаем над мобильным приложением маркетплейса, который предполагает интеграции с различными партнерами (от банков до образовательных учреждений). На пути развития продукта столкнулись с задачей: создать для партнеров SDK, включающий часть функциональности основного приложения. Например, отдельные механики, такие как авторизации и продуктовые фичи, состоящие из множества экранов. 

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

Но есть проблема: каждый отдельный модуль может создавать и публиковать только один артефакт, содержащий код и ресурсы конкретного модуля. То есть вместе с aar SDK нам необходимо поставить партнеру ещё массу дочерних библиотек, или же опубликовать их в репозитории. В любом случае для каждого артефакта придётся устанавливать свою версию и отдельно публиковать его при любых незначительных изменениях. Чтобы реализовать такое в уже существующем проекте, требуется немало усилий; возможно, тщательный рефакторинг, что может негативно сказаться на рабочем процессе в целом.

Альтернативное решение — перенести весь код, необходимый для SDK, в один модуль, то есть сделать форк. Тогда будет создаваться и публиковаться один артефакт, не нужно менять уже существующие модули. Но есть нюанс, связанный с обновлением данного модуля: если мы нашли баг в оригинальном модуле, то фикс нужно добавить и в SDK. Опять же, сам перенос кода в один модуль может занять массу времени, при этом никто не застрахован от ошибок; есть риск появления новых проблем, на решение которых также придется тратить ресурсы.

Собираем fat-aar — но как?

Учитывая вышесказанное, мы пришли к наиболее подходящему для нас варианту: сборке одного артефакта из уже существующих модулей. Полученный таким образом артефакт обычно называют fat-aar. В теории подход решает все наши проблемы: не нужно трогать код уже существующих модулей, настраивать их публикацию. Мы получаем один артефакт, включающий все необходимое для своей работы, который удобно распространять и публиковать. Плюс не раскрываем лишних деталей того, как устроен наш проект.

Задачу сформулировали следующим образом: необходимо создать инструмент, который позволит нам собирать один артефакт из множества модулей. При этом важно учесть следующее:

  1. В итоговый aar-архив, помимо прямых зависимостей, должны попадать все дочерние зависимости (транзитивные).

  2. В случае изменения зависимостей в каком-либо модуле, нам не нужно вручную менять конфигурацию, всё должно происходить автоматически.

  3. Мы должны иметь возможность публиковать полученный артефакт, генерируя при этом все необходимые метаданные.

К сожалению, Google не предоставляет инструмента для создания fat-aar. И, судя по всему, в скором времени ждать его не стоит. Некоторые наработки уже существуют, например FusedLibraryPlugin, но к использованию этот инструмент совершенно не готов.

Оставалось лишь обратиться к opensource-решениям. Одни из самых популярных: fat-aar-android, android-fat-aar, fat-aar-plugin и их многочисленные форки. Эти библиотеки имеют целый ряд недоработок и нерешенных проблем, плюс давно не поддерживаются и не работают с последними версиями AGP

В ходе непродолжительных поисков обнаружили библиотеку Grease. Относительно свежий проект, активно поддерживаемый и на поверку хорошо выполняющий свою задачу. 

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

dependencies { // при сборке это содержимое модулей :another и :other // будет помещено в итоговый aar архив grease(project(":another")) grease(project(":other"))  // ничего нам не мешает подключать другие зависимости // содержимое которых не попадет в итоговый архив implementation("...") }

Есть и другой способ добавления модулей, через greaseTree. С помощью этой команды в итоговый архив помещаются все транзитивные зависимости выбранного модуля. В теории это должно было закрыть п.1 из списка выше, но на практике оказалось иначе.

Проблема с greeseTree в том, что плагин пытается включить вообще все зависимости, определённые в дочерних модулях, без возможности их фильтрации. И даже если бы мы могли исключить ряд транзитивных зависимостей, это не сильно бы помогло. Надо заранее знать, что исключать, и в случаем с крупным проектом вручную составить такой список может быть непросто. То есть в любом случае необходимо вносить доработки со своей стороны. В итоге greaseTree мы решили не использовать.

Проблема с включением всех дочерних модулей оставалась нерешённой.

Для начала нам нужно было составить их список. Попытались сделать это вручную, но быстро поняли, что способ нам не подходит. Приходилось проходить по скрипту сборки каждого модуля, выписывать все зависимости, убирать повторы. И при каждом изменении зависимостей в каждом дочернем модуле, повторять процесс. В итоге мы получали примерно такую конфигурацию:

dependencies { grease(project(":first")) grease(project(":second")) grease(project(":third")) grease(project(":fourth")) grease(project(":fifth")) // ...  implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.material3) implementation(libs.androidx.ui.tooling.preview) debugImplementation(libs.androidx.ui.tooling) }

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

В итоге списки получили, пройдясь по зависимостям из стандартных конфигураций api и implementation. Например, следующий код выводит список всех зависимостей, определённых для данного модуля:

// нас интересуют только стандартные конфигурации api и implementation val configurationNames = listOf("api", "implementation") val configurations = project.configurations     .filter { it.name in configurationNames } for (configuration in configurations) {     // в каждой конфигурации есть свой список зависимостей     for (dependency in configuration.dependencies) {         println(dependency.name)     } }

Выполнив код выше для данной конфигурации:

dependencies {     implementation(project(":first"))     implementation(project(":second"))      implementation(platform("androidx.compose:compose-bom:2025.02.00"))     implementation("androidx.compose.material3:material3")     implementation("androidx.compose.ui:ui-tooling-preview")     debugImplementation("androidx.compose.ui:ui-tooling") }

Мы получим следующий вывод:

DefaultProjectDependency{identityPath=':first', configuration='default'} DefaultProjectDependency{identityPath=':second', configuration='default'} DefaultExternalModuleDependency{group='androidx.compose', name='compose-bom', version='2025.02.00', configuration='default'} DefaultExternalModuleDependency{group='androidx.compose.material3', name='material3', version='null', configuration='default'} DefaultExternalModuleDependency{group='androidx.compose.ui', name='ui-tooling-preview', version='null', configuration='default'}

Чтобы получить зависимости дочерних модулей, для них надо выполнить похожий код. Для этого получаем объект Project — с помощью метода dependencyProject из интерфейса ProjectDependency (важно: способ перестанет работать, начиная с 9 версии gradle, подробности можно найти здесь). Немного модифицируем пример выше:

 // нас интересуют только стандартные конфигурации api и implementation val configurationNames = listOf("api", "implementation")  // проходимся по всему дереву зависимостей val visited = mutableSetOf<Project>() val queue = ArrayDeque<Project>() queue.add(project) while (queue.isNotEmpty()) {     val current = queue.removeFirst()     val configurations = current.configurations         .filter { it.name in configurationNames }     for (configuration in configurations) {         // в каждой конфигурации есть свой список зависимостей         println(current.name)     for (dependency in configuration.dependencies) {             println(dependency)             // если это локальная зависимость, добавляем ее в очередь на обработку             if (dependency is ProjectDependency) {             val dependencyProject = dependency.dependencyProject             if (visited.add(dependencyProject)) {                     queue.add(dependencyProject)             }             }     }     println()     } }  

В целом пример рабочий, но на практике он будет выдавать некорректный результат. Дело в том, что на момент его вызова дочерние проекты еще не проинициализированы, соответственно, у них отсутствуют конфигурации, в которых должны содержаться зависимости. Чтобы это исправить, нужно запустить данный код уже после того, как все проекты были проинициализированы. Для этого регистируем коллбек, с помощью функции projectsEvaluated интерфейса Gradle

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

project.gradle.projectsEvaluated {     // нас интересуют только стандартные конфигурации api и implementation     val configurationNames = listOf("api", "implementation")      // проходимся по всему дереву зависимостей     val dependencies = mutableSetOf<Dependency>()     val visited = mutableSetOf<Project>()     val queue = ArrayDeque<Project>()     queue.add(project)     while (queue.isNotEmpty()) {         val project = queue.removeFirst()         val filteredConfigurations = project.configurations             .filter { it.name in configurationNames }         for (configuration in filteredConfigurations) {             for (dependency in configuration.dependencies) {                 if (dependency is ProjectDependency) {                     if (visited.add(dependency.dependencyProject)) {                         queue.add(dependency.dependencyProject)                     }                 }                 dependencies.add(dependency)             }         }     }      for (dependency in dependencies) {         println(dependency)     } }

Следующая проблема, которую нам предстояло решить: настройка публикации нашего fat-aar. Для публикации артефактов в репозитории в gradle уже встроены специальные плагины maven-publish и ivy-publish. С их помощью можно настроить все необходимые параметры для публикации: id модуля, id артефакта, версию, а также сконфигурировать метаданные, в которых будет содержаться информация о необходимых зависимостях. 

Для генерации метаданных в gradle создан специальный интерфейс SoftwareComponent. AGP по умолчанию создает объекты, имплементирующие данный интерфейс, для каждого варианта сборки. Их можно получить с помощью функции getComponents интерфейса Project. Что позволяет нам быстро и просто настроить публикацию, например:

publishing {     publications {         register<MavenPublication>("release") {             groupId = "com.company"             artifactId = "my-library"             version = "1.0"              afterEvaluate {                 // добавляем компонент для публикации                 // включая все необходимые метаданные                 from(components["release"])             }         }     } }

Пример выше будет генерировать неполные метаданные, то есть в публикации будут указаны только те зависимости, которые определены непосредственно для данного модуля, транзитивных зависимостей в метаданных не будет. 

Эту проблему можно решить, модифицировав POM-файл с помощью функции withXml —  добавив все внешние зависимости, полученные ранее:

afterEvaluate {     // добавляем компонент для публикации     // включая все необходимые метаданные     from(components["release"])     // изменяем сгенерированный pom файл     pom.withXml {         // генерируем новый блок зависимостей         val dependenciesNode = Node(null, "dependencies")         for (dependency in dependencies) {             if (dependency !is ModuleDependency) continue             with(dependenciesNode.appendNode("dependency")) {                 appendNode("groupId", dependency.group)                 appendNode("artifactId", dependency.name)                 appendNode("version", dependency.version)                 appendNode("scope", "runtime")             }         }         val originalNode = asNode().getAt(QName("*", "dependencies", "*"))[0] as Node         // заменяем оригинальный блок зависимостей         originalNode.replaceNode(dependenciesNode)     } }

Используя пример выше, при публикации библиотеки мы получим корректные метаданные для maven-репозитория, однако метаданные gradle останутся некорректными. 

Чтобы получить корректные метаданные, мы должны создать кастомный SoftwareComponent, куда будут входить все необходимые внешние зависимости. Для этого необходимо создать AdhocComponentWithVariants и добавить в него конфигурации с нашими внешними зависимостями. AGP во время инициализации проекта генерирует конфигурации специально для публикации, например для релизной сборки:

  • releaseApiElements-published — вариант для сборки

  • releaseRuntimeElements-published — вариант для рантайма

  • releaseSourceElements-published — вариант для исходников

На основе этих конфигураций генерируются соответствующие компоненты. Все, что нам нужно сделать, —  на их основе создать новые конфигурации с новым списком зависимостей. Создание новых компонентов необходимо производить с помощью SoftwareComponentFactory. Использования этой фабрики возможно только через создание плагина, поэтому код будет выглядеть так:

abstract class SdkPlugin @Inject constructor(     private val componentFactory: SoftwareComponentFactory ) : Plugin<Project> {      override fun apply(target: Project) {         // в данном примере опустим код получения зависимостей         val moduleDependencies = ...                  target.gradle.projectsEvaluated {             val configurations = listOf(                 "releaseApiElements-published",                 "releaseRuntimeElements-published",                 "releaseSourcesElements-published"             ).map { target.configurations.getByName(it) }              // Начинаем формировать список новых конфигураций для публикации             val newPublishConfigurations = mutableSetOf<Configuration>()             for (configuration in configurations) {                 // Создаем новую конфигурацию с тем же названием, но с приставкой 'my'                 val newConfiguration = target.configurations                     .create("my-${configuration.name}")                 newPublishConfigurations.add(newConfiguration)                  // Копируем атрибуты из оригинальной конфигурации в новую                 for (key in configuration.attributes.keySet()) {                     @Suppress("UNCHECKED_CAST")                     key as Attribute<Any?>                     val value = configuration.attributes.getAttribute(key)                     if (value != null) {                         newConfiguration.attributes.attribute(key, value)                     }                 }                  // Добавляем все дочерние зависимости в новую конфигурацию                 for (dependency in moduleDependencies) {                     newConfiguration.dependencies.add(dependency)                 }             }              // Создаем новый компонент для публикации, и добавляем в него наши новые конфигурации             // Таким образом, при публикации будет сгенерированы колрректные методанные             val sdkComponent = componentFactory.adhoc("sdk")             for (configuration in newPublishConfigurations) {                 sdkComponent.addVariantsFromConfiguration(configuration) {}             }             target.components.add(sdkComponent)         }     } }

После применения плагина, появляется возможность настроить публикацию следующим образом:

       register<MavenPublication>("release") {             groupId = "com.company"             artifactId = "my-library"             version = "1.0"              gradle.projectsEvaluated {                 from(components["sdk"])             }         }

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

В целом мы решили все вопросы и проблемы, все готово для решения нашей задачи: сборки и публикации многомодульных библиотек. Чего нам не хватает, так это проработать нюансы работы с различными вариантами сборки. В библиотеке может быть определено множество flavors и buildTypes, из которых и формируются buildVariants. Ничего сложного в реализации их поддержки нет, поэтому подробно на этом останавливаться не будем. 

Итоговый код нашего плагина:

private val DefaultElementsConfigurations = listOf("api", "runtime", "sources")  abstract class SdkPlugin @Inject constructor(     private val componentFactory: SoftwareComponentFactory ) : Plugin<Project> {      override fun apply(target: Project) {         // Применяем плагин только после того как будет применен плагин grease         target.plugins.withId("io.deepmedia.tools.grease") {             val libraryExtension = target.extensions.getByType(LibraryExtension::class.java)             // Производить настройку плагина нужно после того как все проекты были настроены, но             // до того как перехода в фазу выполнения (Execution phase)             // Если мы начнем настройку раньше, то зависимости некоторых проектов еще не будут             // определены             // Если мы начнем позже, уже в фазе выполнения, то мы не сможем настроить конфигурации,             // их можно настраивать только в фазе конфигурации (Configuration phase)             target.gradle.projectsEvaluated {                 // Настраиваем конфигурации для каждого отдельного варианта сборки                 for (variant in libraryExtension.libraryVariants) {                     setupVariant(target, variant)                 }             }         }     }      @Suppress("DEPRECATION")     private fun setupVariant(target: Project, variant: LibraryVariant) {         // Список проектов необходим для конфигурации grease         // Список модулей необходим для настройки публикации         val projects = mutableSetOf<Project>()         val moduleDependencies = mutableSetOf<ModuleDependency>()         // Формируем список всех зависимостей данного модуля, включая зависимости дочерних проектов         for (dependency in target.allDependencies(variant)) {             when (dependency) {                 is ProjectDependency -> {                     projects.add(dependency.dependencyProject)                 }                 is ModuleDependency -> {                     moduleDependencies.add(dependency)                 }             }         }          // Добавляем все проекты в конфигурацию grease, для данного модуля и варианта сборки         for (project in projects) {             target.dependencies.add(camelCase("grease", variant.name), project)         }          // Составляем список названий конфигураций для публикации для даненого проекта и варианта         // сборки         // В ходе работы agp формирует 3 такие конфигурации:         // - {buildVariant}ApiElements-published - для сборки         // - {buildVariant}RuntimeElements-published - для рантайма         // - {buildVariant}SourceElements-published - для исходников         val publishConfigurationNames = DefaultElementsConfigurations             .map { configurationName ->                 camelCase(variant.name, configurationName, "elements-published")             }          // Находим все конфигурации указанные выше в текущем проекте         val publishConfigurations = target.configurations             .filter { it.name in publishConfigurationNames }          // Начинаем формировать список новых конфигураций для публикации         val newPublishConfigurations = mutableSetOf<Configuration>()         for (configuration in publishConfigurations) {             // Создаем новую конфигурацию с тем же названием, но с приставкой 'sdk'             val newConfiguration = target.configurations                 .create(camelCase("sdk", configuration.name))             newPublishConfigurations.add(newConfiguration)              // Копируем атрибуты из оригинальной конфигурации в новую             for (key in configuration.attributes.keySet()) {                 @Suppress("UNCHECKED_CAST")                 key as Attribute<Any?>                 val value = configuration.attributes.getAttribute(key)                 if (value != null) {                     newConfiguration.attributes.attribute(key, value)                 }             }              // Добавляем артефакты из оригинальной конфигурации             newConfiguration.artifacts.addAll(configuration.artifacts)              // Добавляем все дочерние зависимости в новую конфигурацию             for (dependency in moduleDependencies) {                 newConfiguration.dependencies.add(dependency)             }         }          // Создаем новый компонент для публикации, и добавляем в него наши новые конфигурации         // Таким образом, при публикации будет сгенерированы колрректные методанные         val sdkComponent = componentFactory.adhoc(camelCase("sdk", variant.name))         for (configuration in newPublishConfigurations) {             sdkComponent.addVariantsFromConfiguration(configuration) {}         }         target.components.add(sdkComponent)     } }  // Функция для получения общего списка всех зависимостей данного и всех дочерних проектов, // определенных в указанных конфигурациях fun Project.allDependencies(configurationNames: Set<String>): Set<Dependency> {     val dependencies = mutableSetOf<Dependency>()     val visited = mutableSetOf<Project>()     // Рекурсивно проходимся по всем зависимостям проектов, начиная с текущего     val queue = ArrayDeque<Project>()     queue.add(this)     while (queue.isNotEmpty()) {         val project = queue.removeFirst()         val filteredConfigurations = project.configurations             .filter { it.name in configurationNames }         for (configuration in filteredConfigurations) {             for (dependency in configuration.dependencies) {                 if (dependency is ProjectDependency) {                     if (visited.add(dependency.dependencyProject)) {                         queue.add(dependency.dependencyProject)                     }                 }                 dependencies.add(dependency)             }         }     }     return dependencies }  private val DefaultConfigurationNames = listOf("api", "implementation")  @Suppress("DEPRECATION") // Функция для получения общего списка всех зависимостей данного и всех дочерних проектов, // для выбранного варианта сборки fun Project.allDependencies(variant: LibraryVariant): Set<Dependency> {     // Cтандартные конфигурации api и implementation     val configurationNames = DefaultConfigurationNames.toMutableSet()      // Добавляем конфигурации для выбранного типа сборки, например для debug:     // - debugApi     // - debugImplementation     val buildType = variant.buildType.name     for (name in DefaultConfigurationNames) {  configurationNames.add(camelCase(buildType, name))     }      // Добавляем конфигурации для выбранного flavor, например для demo и типа сборки debug:     // - demoApi     // - demoImplementation     // - demoDebugApi     // - demoDebugImplementation     val flavorName = variant.flavorName     if (flavorName != null) {         for (name in DefaultConfigurationNames) {             configurationNames.add(camelCase(flavorName, name))         }          for (name in DefaultConfigurationNames) {             configurationNames.add(camelCase(flavorName, buildType, name))         }     }      return allDependencies(configurationNames) }  private fun camelCase(vararg names: String): String = buildString {     if (names.isEmpty()) return@buildString     append(names[0].replaceFirstChar { it.lowercaseChar() })     for (i in 1 until names.size) {         append(names[i].replaceFirstChar { it.uppercaseChar() })     } } 

Как наш плагин работает на практике

В целом плагин уже полностью покрывает наши задачи. Дополнительно можно добавить экстеншены для фильтрации зависимостей, исключения/включения исходников (сейчас они всегда публикуются), таски для вывода справочной информации, например, список всех зависимостей — по аналогии с таской dependencies

Реализация таски по выводу итоговых зависимостей:

abstract class SdkDependenciesTask : DefaultTask() {      @get:Internal     internal var buildVariant: String = ""      @TaskAction     fun action() {         val logger = project.logger         val libraryExtension = project.extensions.getByType(LibraryExtension::class.java)         val variant = libraryExtension.libraryVariants.find { it.name == buildVariant }             ?: throw IllegalArgumentException("Library variant '$buildVariant' was not found")          val projectDependencies = sortedSetOf<String>()         val moduleDependencies = sortedSetOf<String>()         for (dependency in project.allDependencies(variant)) {             when (dependency) {                 is ProjectDependency -> {                     projectDependencies.add(                         dependency.dependencyProject.path                     )                 }                 is ModuleDependency -> {                     moduleDependencies.add(                         "${dependency.group}:${dependency.name}:${dependency.version}"                     )                 }             }         }          logger.quiet("Complete projects list:")         for (dependency in projectDependencies) {             logger.quiet(dependency)         }          logger.quiet("Complete modules list:")         for (dependency in moduleDependencies) {             logger.quiet(dependency)         }     } }

Функция для регистрации нашей таски в плагине для выбранного варианта сборки:

private fun setupTasks(target: Project, variant: LibraryVariant) {     target.tasks.register<SdkDependenciesTask>(camelCase("sdkDependencies", variant.name)) {         group = "sdk"         description = "Generate sdk`s dependencies for '${variant.name}' build variant"         buildVariant = variant.name     } }

Рассмотрим небольшой пример работы данного плагина. Представим, что у нас есть несколько модулей, со следующей конфигурацией:

// конфигурация модуля :fataar // данный модуль будет отвечать за сборку итогового артефакта  import org.jetbrains.kotlin.config.JvmTarget  plugins {     alias(libs.plugins.kotlin.android)     alias(libs.plugins.android.library)     alias(libs.plugins.gradle.lifecycle)     // плагины для создания fat-aar     alias(libs.plugins.deepmedia.grease)     alias(libs.plugins.sdk)     `maven-publish` }  android {     namespace = "com.example.fataar"      compileSdk = 35      kotlinOptions {         jvmTarget = JvmTarget.JVM_1_8.description     }      // добавляем варианты сборки     flavorDimensions += "test"     productFlavors {         create("another") {             dimension = "test"         }          create("other") {             dimension = "test"         }     } }  dependencies {     // добавляем в зависимости два дочерних модуля      implementation(project(":first"))     implementation(project(":second"))      // добавляем внешние зависимости     implementation(platform(libs.androidx.compose.bom))     implementation(libs.androidx.material3)     implementation(libs.androidx.ui.tooling.preview)     debugImplementation(libs.androidx.ui.tooling) }  publishing {     publications {         // публикация итогового артефакта из варианта сборки anotherDebug         register<MavenPublication>("anotherDebug") {             groupId = "com.example.fataar"             artifactId = "sdk"             version = "0.1.0"              // используем компонент созданный нашим плагином sdkAnotherDebug             // обязательно помещаем вызов form в projectsEvaluated             // так как компонент становиться доступен только после конфигурации всех проектов             gradle.projectsEvaluated {                 from(components["sdkAnotherDebug"])             }         }     } } 
// конфигурация модуля :first  import org.jetbrains.kotlin.config.JvmTarget  plugins {     alias(libs.plugins.android.library)     alias(libs.plugins.kotlin.android)     alias(libs.plugins.gradle.lifecycle) }  android {     namespace = "com.example.first"      compileSdk = 35      kotlinOptions {         jvmTarget = JvmTarget.JVM_1_8.description     } }  dependencies {     implementation(project(":second"))      implementation(platform(libs.okhttp.bom))     implementation(libs.okhttp)     implementation(libs.logging.interceptor) }
// конфигурация модуля :second  import org.jetbrains.kotlin.config.JvmTarget  plugins {     alias(libs.plugins.android.library)     alias(libs.plugins.kotlin.android)     alias(libs.plugins.gradle.lifecycle) }  android {     namespace = "com.example.second"      compileSdk = 35      kotlinOptions {         jvmTarget = JvmTarget.JVM_1_8.description     } }  dependencies {     implementation(project(":third"))      implementation(libs.kotlinx.coroutines.core) }
// конфигурация модуля :third  import org.jetbrains.kotlin.config.JvmTarget  plugins {     alias(libs.plugins.android.library)     alias(libs.plugins.kotlin.android)     alias(libs.plugins.gradle.lifecycle) }  android {     namespace = "com.example.third"      compileSdk = 35      kotlinOptions {         jvmTarget = JvmTarget.JVM_1_8.description     } }  dependencies {     implementation(libs.androidx.core.ktx) }
// конфигурация модуля :fourth  import org.jetbrains.kotlin.config.JvmTarget  plugins {     alias(libs.plugins.android.library)     alias(libs.plugins.kotlin.android)     alias(libs.plugins.gradle.lifecycle) }  android {     namespace = "com.example.fourth"      compileSdk = 35      kotlinOptions {         jvmTarget = JvmTarget.JVM_1_8.description     } }  dependencies {     implementation(project(":third"))      implementation(platform(libs.okhttp.bom))     implementation(libs.okhttp)     implementation(libs.logging.interceptor) }

Модуль fataar напрямую зависит от модулей first и second и косвенно от модуля third. Код из модуля fourth в итоговый артефакт не попадёт, так как его нет в итоговом графе зависимостей для fat-aar.

При публикации fat-aar, например в mavenLocal, будут сгенерированы следующие файлы:

  • sdk-0.1.0.aar — итоговый архив

  • sdk-0.1.0.module — метаданные gradle

  • sdk-0.1.0.pom — метаданные maven

  • sdk-0.1.0-sources.jar — архив с исходниками

Открыв файл с метаданными gradle, мы увидим корректные варианты сборки, с корректными зависимостями и атрибутами:

{   "formatVersion": "1.1",   "component": {     "group": "com.example.fataar",     "module": "sdk",     "version": "0.1.0",     "attributes": {       "org.gradle.status": "release"     }   },   "createdBy": {     "gradle": {       "version": "8.10.2"     }   },   "variants": [     {       "name": "sdkAnotherDebugApiElements-published",       "attributes": {         "com.android.build.api.attributes.BuildTypeAttr": "debug",         "com.android.build.api.attributes.ProductFlavor:test": "another",         "org.gradle.category": "library",         "org.gradle.jvm.environment": "android",         "org.gradle.usage": "java-api",         "org.jetbrains.kotlin.platform.type": "androidJvm",         "test": "another"       },       "dependencies": [         {           "group": "androidx.compose.ui",           "module": "ui-tooling"         },         {           "group": "androidx.compose",           "module": "compose-bom",           "version": {             "requires": "2025.02.00"           },           "attributes": {             "org.gradle.category": "platform"           }         },         {           "group": "androidx.compose.material3",           "module": "material3"         },         {           "group": "androidx.compose.ui",           "module": "ui-tooling-preview"         },         {           "group": "com.squareup.okhttp3",           "module": "okhttp-bom",           "version": {             "requires": "4.12.0"           },           "attributes": {             "org.gradle.category": "platform"           }         },         {           "group": "com.squareup.okhttp3",           "module": "okhttp"         },         {           "group": "com.squareup.okhttp3",           "module": "logging-interceptor"         },         {           "group": "org.jetbrains.kotlinx",           "module": "kotlinx-coroutines-core",           "version": {             "requires": "1.10.1"           }         },         {           "group": "androidx.core",           "module": "core-ktx",           "version": {             "requires": "1.15.0"           }         }       ],       "files": [         {           "name": "sdk-0.1.0.aar",           "url": "sdk-0.1.0.aar",           "size": 1508,           "sha512": "fce37611072ce8e7e132968e26a6855fd67f4cf8648cf966c1e719140ab16cdad65ce8da46eda5601f37ac52988bdb091c4c3158068debfc853c0f9042fc6042",           "sha256": "213bc965f4878571d03c4e70681a0dfb5793cc87812f14fc57c8fa5e6c521a14",           "sha1": "2d382683bbe63f9536608a4779cb014e0971fe5e",           "md5": "699ed5a623041c15b9c9912cfb205414"         }       ]     },     // остальные варианты опустим для краткости   ] }

Здесь мы можем видеть что все метаданные корректно сгенерировались, все необходимые зависимости есть в списке, лишнего нет. То есть мы получили такой же результат, как если бы весь код находился в одном единственном модуле.

***

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

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

Спасибо за внимание! На ваши вопросы буду рад ответить в комментах.



ссылка на оригинал статьи https://habr.com/ru/articles/892416/