Я — Денис, Middle Android-разработчик в «Лайв Тайпинге». В этой статье я расскажу о современном подходе организации зависимостей в Android. Вы узнаете как использовать version catalog и convention plugin в вашем проекте.

История организации зависимостей в Android

Ant

Основное известное использование Ant — это сборка Java-приложений. Ant предоставляет ряд встроенных задач, позволяющих компилировать, собирать, тестировать и запускать Java-приложения. Ant также можно эффективно использовать для создания приложений, отличных от Java, например приложений C или C++.

Проекты по разработке программного обеспечения, которым требуется решение, сочетающее инструмент сборки и управление зависимостями, могут использовать Ant в сочетании с Apache Ivy (менеджер зависимостей).

Target (цели)

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

В Ant target не имеют заранее определённых наименований, однако желательно придерживаться общепринятых стандартов, например:

  • compile для компиляции;

  • test для проверки;

  • run для запуска;

  • clean для удаления папок.

Target может зависеть от других target, и Apache Ant гарантирует, что эти другие target были выполнены раньше текущего target. Например, у вас может быть target для компиляции и target для создания дистрибутива. Вы можете собрать дистрибутив только после того, как вы сначала скомпилировали его, поэтому target создания дистрибутива зависит от target компиляции.

<target name="A"/>
<target name="B" depends="A"/>
<target name="C" depends="B"/>
<target name="D" depends="C,B,A"/>

Если запустить выполнение target D, то target выполнятся в таком порядке: A → B → C → D.

Пример использования

Рассмотрим простейшую программу, написанную на Java.

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Создадим для программы сценарий build.xml.

<?xml version="1.0"?>
<project name="HelloWorld" default="run">
    <target name="compile">
        <mkdir dir="build/classes"/>
        <javac destdir="build/classes" includeantruntime="false">
            <src path="src"/>
        </javac>
    </target>
    <target name="run" depends="compile">
        <java classname="HelloWorld" classpath="build/classes"/>
    </target>
    <target name="clean">
        <delete dir="build"/>
    </target>
</project>

Сценарий выше содержит три target (команды):

  • compile (компиляция файла(ов) .java);

  • run (запуск файла .class);

  • clean (удаление папок с результатами компиляции).

Maven

Apache Maven — фреймворк для автоматизации сборки проектов на основе описания их структуры в файлах на языке POM (Project Object Model), являющемся подмножеством XML.

Проект Apache Maven является частью Apache Software Foundation. Название Maven является словом из языка идиш, смысл которого можно примерно выразить как «собиратель знания». 

Год выхода: 2008.

Maven сочетает в себе возможности Ant (билдить и копировать файлы, проводить тесты), но кроме этого он помогает решать зависимости библиотек проекта, имеет вполне формальный жизненный цикл, имеет множество плагинов и занимает всё те же три символа в командной строке: mvn vs ant.

Maven используется для построения и управления проектами, написанными на Java, C#, Ruby, Scala и других языках.

Жизненный цикл

Жизненный цикл Maven содержит задачи — аналог target (цели) у Ant в отличие от target у Ant, жизненный цикл Maven имеет заранее определённые задачи и зависимости, определяющие порядок их вызова.

Существует несколько независимых порядков выполнения: clean (для очистки проекта), site (для генерации проектной документации) и default.

Перечислю некоторые default задачи:

  1. compile — компилирует исходники;

  2. package — упаковывает скомпилированные классы;

  3. install — загоняет пакет в локальный репозиторий, откуда пакет будет доступен для использования как зависимость в других проектах;

  4. deploy — отправляет пакет на удаленный production сервер, откуда другие разработчики его могут получить и использовать.

Maven конфигурируется файлом pom.xml, который может содержать блок <dependencies/>. В нём описываются библиотеки, которые нужны проекту для полноценного функционирования. При указании зависимостей можно указывать не только имя библиотеки, но и ее версию.

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

Maven базируется на plugin-архитектуре, которая позволяет применять плагины для различных задач для данного проекта, без необходимости их в явном виде инсталлировать.

Пример

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

Пример содержимого файла pom.xml:

<project>
  <!-- версия модели для POM-ов Maven 2.x всегда 4.0.0 -->
  <modelVersion>4.0.0</modelVersion>
  
  <!-- координаты проекта, то есть набор значений, который
       позволяет однозначно идентифицировать этот проект -->
  
  <groupId>com.mycompany.app</groupId>
  <artifactId>my-app</artifactId>
  <version>1.0</version>

  <!-- зависимости от библиотек -->
  
  <dependencies>
    <dependency>
    
      <!-- координаты необходимой библиотеки -->
      
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>3.8.1</version>
      
      <!-- эта библиотека используется только для запуска и компилирования тестов -->
      
      <scope>test</scope>
      
    </dependency>
  </dependencies>
</project>

Gradle

Gradle представляет предметно-ориентированный язык на основе Groovy и Kotlin. Gradle использует направленный ациклический граф, чтобы определить порядок, в котором могут выполняться задачи, посредством обеспечения управления зависимостями. Он работает на виртуальной машине Java.

Основатель и генеральный директор Ханс Доктер изначально хотел назвать проект «Cradle» (колыбель, люлька). Однако, чтобы сделать имя уникальным и менее «младенческим», он вместо этого выбрал «Gradle», взяв букву «G» от Groovy.

Gradle не привязан к конкретной платформе. К тому же, в системе используют разные языки программирования. Наиболее популярные — Groovy DSL и Kotlin.

Основные понятия при работе с Gradle — плагины и задачи. Их зависимость такова: плагины предоставляют и создают задачи. Задачи — те действия, которые надо сделать.

Файл build.gradle — скрипт, где указаны библиотеки, фреймворки, плагины и задачи конкретного проекта.

Наше время — Gradle kts

Kotlin DSL от Gradle предоставляет синтаксис, альтернативный традиционному Groovy DSL, с расширенными возможностями редактирования в поддерживаемых IDE, с превосходной поддержкой контента, рефакторингом, документацией и многим другим. В этой главе подробно описаны основные конструкции Kotlin DSL и способы их использования для взаимодействия с Gradle API.

Как и аналог на основе Groovy, Kotlin DSL реализован поверх Java API Gradle. Все, что вы можете прочитать в скрипте Kotlin DSL, — это код Kotlin, скомпилированный и выполненный Gradle. Многие объекты, функции и свойства, которые вы используете в своих сценариях сборки, поступают из API Gradle и API применяемых плагинов.

Файлы сценариев Kotlin DSL используют .gradle.kts расширение имени файла.

Чтобы активировать Kotlin DSL, просто используйте .gradle.kts расширение для ваших скриптов сборки вместо .gradle. Это также относится к файлу настроек — например settings.gradle.kts — и сценариям инициализации .

Version catalog и как с ним работать

С выходом Android studio Iguana при создании проекта из шаблона вместо объявления зависимостей непосредственно в .gradle.kts, все зависимости стали располагаться в .toml файле. Вот, что об этом сказано в документации:

Version catalog позволяет добавлять и поддерживать зависимости и плагины масштабируемым образом. Он упрощает управление зависимостями и плагинами, особенно когда у вас есть несколько модулей. Вместо жёсткого закодирования названий и версий зависимостей в отдельных файлах сборки и обновления каждой записи при необходимости обновления зависимости, вы можете создать центральный version catalog, на который могут ссылаться различные модули.

Файл .toml расположен в корне проекта, если вы находитесь в представлении Android, и в папке gradle, если в представлении Project. Структура файла в только что созданном проекте выглядит так:

Untitled

Ниже на примере покажу, что поменялось. Раньше, чтобы добавить зависимость в проект мы делали так:

  1. добавим процессор аннотаций KSP в .gradle.kts:

    plugins {
        id("com.google.devtools.ksp") version "1.8.10-1.0.9" apply false
    }
  2. добавим плагин для KSP:

    plugins {
      id("com.google.devtools.ksp")
    }
  3. в блоке dependencies добавим зависимости для библиотеки Room и annotationProcessor:

    dependencies {
        val room_version = "2.6.1"
        implementation("androidx.room:room-runtime:$room_version")
        annotationProcessor("androidx.room:room-compiler:$room_version")
        ksp("androidx.room:room-compiler:$room_version")
    }

С появлением version catalog можно делать так:

  1. добавим процессор аннотаций KSP в .gradle.kts:

    plugins {
      alias(libs.plugins.ksp) apply false
    }
  2. добавим плагин для KSP:

    plugins {
      alias(libs.plugins.ksp)
    }
  3. в блоке dependencies добавим зависимости для библиотеки Room и annotationProcessor:

    dependencies {
        implementation(libs.androidx.room)
        annotationProcessor(libs.androidx.room.annotation.processor)
        ksp(libs.androidx.room.annotation.processor)
    }

Так выглядит .toml файл после добавления зависимостей:

Untitled
Источник картинки — https://habr.com/ru/articles/801287/

Новый способ организации зависимостей не мешает вам смешивать старый и новый подход. Android studio подсветит зависимости, которые добавлены непосредственно в .gradle.kts старым способом и предложит перенести его в .toml. Это не всегда срабатывает гладко, особенно если вы используете сложную логику организации зависимостей в проекте — convention plugin.

Синтаксис

В .toml есть четыре области для работы с зависимостями:

  • [versions] — переменные для версий зависимостей;

  • [libraries] — список библиотек;

  • [bundles] — набор, который позволяет объединить N кол-во библиотек в один вызов;

  • [plugins] — список плагинов.

В [versions] можно объявить версии подключаемых зависимостей. Это переменные строкового типа. Ниже приведу пример:

[versions]
agp = "8.2.2"
androidx-core-ktx = "1.12.0"
androidx-test-ext-junit = "1.1.5"
androidx-espresso-core = "3.5.1"
androidx-lifecycle-runtime-ktx = "2.7.0"
androidx-activity-compose = "1.8.2"
androidx-activity = "1.8.2"
androidx-compose-bom = "2024.02.02"
androidx-navigation = "2.7.7"

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

  • strictlyстрогая версия. Любая версия, не соответствующая этому указанию версии, будет исключена. Она переопределяет любое предыдущее объявление require и очищает предыдущее reject;

  • preferпредпочтительная версия. Определение может дополнять strictly или require. Когда определена, он переопределяет любое предыдущее объявление prefer и очищает предыдущее reject;

  • reject: список отклоненных версий. Заявляет, что конкретные версии не принимаются для модуля. Это вызовет сбой разрешения зависимостей, если единственные выбираемые версии также отклонены;

  • rejectAll: булево значение для отклонения всех версий.

Какая версия(ии) этой зависимости приемлема?

strictly

require

prefer

rejects

Результат выбора

Протестировано с версией 1.5, считается, что все будущие версии должны работать

1.5

Любая версия, начиная с 1.5 до 2.4 — будет валидна

Протестировано с 1.5, ограничение версии в диапазоне

[1.0, 2.0]

1.5

Любая версия между 1.0 и 2.0. Выставит 1.5, если нет конфликтов. Обновление до 2.4 принимаются ?

Не работает на версии 1.4

[1.0, 2.0]

1.5

1.4

Любая версия между 1.0 и 2.0 (исключительно), кроме 1.4. Выставит 1.5, если нет конфликтов. Обновление до 2.4 принимаются ?

Строго на версии 1.5

1.5

1.5, или выкинет исключение

Без разницы, выставит последнею версию

latest.release

Последний релиз на момент сборки ?

Строки, аннотированные замком (?), указывают на то, что использование блокировки зависимостей имеет смысл в этом контексте. Еще одним концептом, связанным с насыщенным объявлением версии, является возможность публикации решенных версий вместо объявленных.

В [libraries] можно объявить список подключаемых библиотек. Внутри используется дополнительный синтаксис:

  • module — указывается полный путь до библиотеки вместе с указанием group. Например, coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil" };

  • group — путь до библиотеки делим на group и name. Например, androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx"};

  • version.ref — сюда передаём переменную с версией зависимости.

Пример объявления библиотек:

[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" }
androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso-core" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "androidx-lifecycle-runtime-ktx" }
androidx-lifecycle-viewmodel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle-runtime-ktx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "androidx-activity-compose" }
activity = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" }

В [bundles] можно объединить N кол-во библиотек в один вызов:

[bundles]
retrofit = [
    "retrofit-core",
    "retrofit-moshi",
]
grpc = [
    "grpc-stub",
    "grpc-okhttp",
    "grpc-protoc-gen-java",
    "annotations-api",
    "protobuf-lite",
]
datastore = [
    "androidx-datastore",
    "datastore-core",
    "datastore-preferences",
    "datastore-preferences-core",
]

В кавычках нужно записать название библиотеки из области [libraries]. После вы можете вызвать это в .gradle.kts вот так:

dependencies {
    implementation(project(":core:feature:domain"))
    implementation(libs.bundles.retrofit)
    implementation(libs.bundles.grpc)
    implementation(libs.bundles.datastore)
}

В [plugins] можно объявить список используемых плагинов:

[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-library = { id = "com.android.library", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
protobuf = { id = "com.google.protobuf", version.ref = "protobufPlugin" }
protobuf-classpath = { id = "com.google.protobuf", version.ref = "protobufGradlePlugin" }

Ниже поделюсь полезными ресурсами для начала работы с version catalog:

Миграция на Version catalog

Начните с создания файла каталога версий. В папке gradle вашего корневого проекта создайте файл libs.versions.toml. Gradle ищет каталог в файле libs.versions.toml по умолчанию, поэтому лучше использовать это стандартное имя.

В файле libs.versions.toml добавьте эти разделы:

[versions]

[libraries]

[plugins]

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

Процесс миграции следующий:

  1. добавьте новую запись в каталог;

  2. синхронизируйте свой проект для Android;

  3. замените предыдущее строковое объявление на безопасный для типов доступ к каталогу.

Добавьте запись для каждой зависимости в разделах versions и libraries файла libs.versions.toml. Синхронизируйте свой проект, а затем замените их объявления в файлах сборки на их имена в каталоге.

Этот фрагмент кода показывает файл build.gradle.kts до удаления зависимости:

dependencies {
    implementation("androidx.core:core-ktx:1.9.0")
}

Этот фрагмент кода показывает, как определить зависимость в файле каталога версий:

[versions]
ktx = "1.9.0"

[libraries]
androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" }

В файле build.gradle.kts каждого модуля, которому требуется зависимость, определите зависимости по именам, которые вы определили в файле TOML.

dependencies {
   implementation(libs.androidx.ktx)
}

Добавьте запись для каждого плагина в разделах версий и плагинов файла libs.versions.toml. Синхронизируйте свой проект, а затем замените их объявления в блоке plugins{} в файлах сборки на их имена в каталоге.

Этот фрагмент кода показывает файл build.gradle.kts до удаления плагина:

// Top-level build.gradle.kts file
plugins {
   id("com.android.application") version "7.4.1" apply false
}
// Module-level build.gradle.kts file
plugins {
   id("com.android.application")
}

Этот фрагмент кода показывает, как определить плагин в файле каталога версий:

[versions]
androidGradlePlugin = "7.4.1"

[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }

Следующий код показывает, как определить плагин com.android.application в файлах build.gradle.ktsверхнего и модульного уровня. Используйте alias для плагинов, которые идут из файла каталога версий, и id для плагинов, которые не идут из файла version catalog.

// Top-level build.gradle.kts
plugins {
   alias(libs.plugins.android.application) apply false
}

// module build.gradle.kts
plugins {
   alias(libs.plugins.android.application)
}

Различия между version catalog и buildSrc

  1. Централизация против децентрализации: version catalog централизует информацию о версиях в одном модуле, в то время как buildSrc децентрализует ее по вашему проекту. В version catalog специальный модуль в структуре вашего проекта отвечает только за хранение версий зависимостей. Эта централизация способствует стандартизированному подходу к управлению версиями.

  2. Простота обслуживания: version catalog просты в обслуживании, поскольку вся информация о версиях сосредоточена в одном месте.

  3. Переиспользование: buildSrc отличается в плане переиспользования. Этот подход позволяет вам создавать пользовательские плагины Gradle и инкапсулировать логику сборки, которой можно поделиться и повторно использовать в нескольких проектах. Если у вас есть набор тасок сборки, плагины или сложная логика, от которой могут извлечь пользу несколько проектов, buildSrc позволяет вам упаковать и поделиться ими эффективно. С другой стороны, version catalog в основном фокусируются на управлении версиями.

Но функциональность version catalog можно расширить до уровня buildSrc и в чём-то даже превзойти. Об этом далее в статье.

Gradle convention plugin и как с ним работать

Вы когда-нибудь оказывались потерянными в сложности модульного проекта, стараясь поддерживать последовательность и организацию по всей вашей кодовой базе? Если да, то вы не одиноки.

Gradle convention plugin — это мощный инструмент для повторного использования конфигурации сборки. Он позволяет определить набор соглашений для проекта или модуля, а затем применить эти convention (соглашения) к другим проектам или модулям. Это может значительно упростить управление сборкой и повысить эффективность.

Есть несколько причин, по которым стоит использовать convention plugin:

  • чтобы поделиться общей конфигурацией сборки между проектами или модулями;

  • чтобы облегчить поддержку конфигураций сборки;

  • чтобы улучшить читаемость и поддерживаемость сценариев сборки;

  • чтобы облегчить автоматизацию задач сборки.

Для начала создадим папку под названием build-logic. Внутри этой папки мы создадим модуль соглашений. Этот модуль можно использовать для хранения единого источника для общих конфигураций модуля.

Затем настроим settings.gradle.kts в convention модуле:

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

rootProject.name = "build-logic"
include(":convention")

Далее, в convention модуле создадим пакет Kotlin. Этот пакет будет служить центром для управления общим модулем и конфигурациями проекта. Например, класс KotlinAndroid, который включает extensions для интеграции Kotlin с Android.

val Project.libs
    get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")

internal fun Project.configureKotlinAndroid(
    commonExtension: CommonExtension<*, *, *, *, *>,
) {
    commonExtension.apply {
        compileSdk =  libs.findVersion("compileSdk").get().toString().toInt()

        defaultConfig {
            minSdk = libs.findVersion("minSdk").get().toString().toInt()
        }

        compileOptions {
            sourceCompatibility = JavaVersion.VERSION_17
            targetCompatibility = JavaVersion.VERSION_1_8
        }
    }

    configureKotlin()

}

internal fun Project.configureKotlinJvm() {
    extensions.configure<JavaPluginExtension> {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }

    configureKotlin()
}

private fun Project.configureKotlin() {
    tasks.withType<KotlinCompile>().configureEach {
        kotlinOptions {
            jvmTarget = JavaVersion.VERSION_17.toString()
            val warningsAsErrors: String? by project
            allWarningsAsErrors = warningsAsErrors.toBoolean()
            freeCompilerArgs = freeCompilerArgs + listOf(
                "-opt-in=kotlin.RequiresOptIn",
                "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
                "-opt-in=kotlinx.coroutines.FlowPreview",
            )
        }
    }
}

Затем мы можем перейти к созданию класса, отвечающего за реализацию интерфейса convention plugin, обозначенного как Plugin<T>. Следуя принципу единого источника истины, мы можем разделить этот класс на отдельные сущности, которые обрабатывают конфигурации Android-приложения, Android-функции и Android-библиотеки.

class AndroidApplicationConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager){
                apply("com.android.application")
                apply("org.jetbrains.kotlin.android")
            }

            extensions.configure<ApplicationExtension> {
                configureKotlinAndroid(this)
                defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
            }
        }
    }
}
class AndroidFeatureConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply {
                apply("wallpaper.android.library")
                apply("wallpaper.android.hilt")
            }

            dependencies {
                add("implementation", project(":domain"))
                add("testImplementation", kotlin("test"))
                add("androidTestImplementation", kotlin("test"))
                add("implementation", libs.findLibrary("kotlinx.coroutines.android").get())
            }
        }
    }
}
class AndroidLibraryConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        with(target) {
            with(pluginManager) {
                apply("com.android.library")
                apply("org.jetbrains.kotlin.android")
            }

            extensions.configure<LibraryExtension> {
                configureKotlinAndroid(this)
                 defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
            }
            dependencies {
                add("androidTestImplementation", kotlin("test"))
                add("testImplementation", kotlin("test"))
            }
        }
    }
}

Как только мы создадим все необходимые плагины конвенций, нужно зарегистрировать id плагина в файле build.gradle.kts в convention модуле.

plugins {
    `kotlin-dsl`
}

group = "com.blank.wallpaper.buildlogic"

java {
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
}
tasks.withType<KotlinCompile>().configureEach {
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_17.toString()
    }
}

dependencies {
    compileOnly(libs.android.gradlePlugin)
    compileOnly(libs.kotlin.gradlePlugin)
}

gradlePlugin {
    plugins {
        register("androidApplication") {
            id = "wallpaper.android.application"
            implementationClass = "AndroidApplicationConventionPlugin"
        }
        register("androidLibrary") {
            id = "wallpaper.android.library"
            implementationClass = "AndroidLibraryConventionPlugin"
        }
        register("androidFeature") {
            id = "wallpaper.android.feature"
            implementationClass = "AndroidFeatureConventionPlugin"
        }
    }
}

Теперь нужно обновить файл setting.gradle.kts, чтобы включить сборку build-logic.

pluginManagement {
    includeBuild("build-logic")
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

После регистрации id можно применить плагин к своим проектам gradle, добавив следующую строку в их файлы build.gradle.kts:

plugins {
    id("wallpaper.android.application")
}

После этого можно использовать convention plugin в своём проекте.

Продвинутая реализация

Каждую крупную зависимость стоит выносить в отдельный класс convention plugin. Например Room. Ниже приведу пример.

class AndroidRoomConventionPlugin : Plugin<Project> {

    override fun apply(target: Project) {
        with(target) {
            pluginManager.apply("androidx.room")
            pluginManager.apply("com.google.devtools.ksp")

            extensions.configure<RoomExtension> {
                schemaDirectory("$projectDir/schemas")
            }

            dependencies {
                add("implementation", libs.findLibrary("room.runtime").get())
                add("implementation", libs.findLibrary("room.ktx").get())
                add("ksp", libs.findLibrary("room.compiler").get())
            }
        }
    }
}

В крупных проектах convention plugin содержит достаточно много независимых классов для управления зависимостями, которые можно переиспользовать от проекта к проекту.

Untitled

В качестве референса реализации convention plugin можно глянуть:

  1. now-in-android от Google;

  2. мой пет-проект.

Заключение

Для большинства сборок buildSrc подходит, особенно начиная с Gradle 8, которые сделали их больше похожими на included builds.

Новый способ организации зависимостей не мешает вам смешивать старый и новый подход.

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

Каждое изменение в buildSrc требует полной перекомпиляции всего проекта. Это может не быть проблемой в вашем проекте, но это огромная проблема в больших многомодульных проектах. Напротив, обновление зависимости с version catalog влияет только на модуль, где она используется (и его зависимый).

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

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

Благодарю за внимание!

Денис Попков

Middle Android разработчик в «Лайв Тайпинге»

Если вы нашли неточности/ошибки в статье или просто хотите дополнить её своим мнением — то прошу в комментарии! Или можете написать мне в Telegram — t.me/MolodoyDenis.

Комментарии (5)


  1. KraleOfRIVIA
    24.03.2024 07:45

    Интересная статья


  1. novoselov
    24.03.2024 07:45
    +1

    ВbuildSrc также поддерживается создание convention plugin'ов в привычном формате *.gradle.kts без необходимости наследования от Plugin и прочего синтаксического мусора.

    // gradle/libs.versions.toml
    [versions]
    jdk = "21"
    kotlin = "1.9.23"
    
    [libraries]
    kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
    
    // buildSrc/settings.gradle.kts
    dependencyResolutionManagement {
        versionCatalogs {
            create("libs") {
                from(files("../gradle/libs.versions.toml"))
            }
        }
    }
    
    // buildSrc/build.gradle.kts
    plugins {
        `kotlin-dsl`
    }
    
    dependencies {
        // Workaround: https://github.com/gradle/gradle/issues/15383
        implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
    
        implementation(libs.kotlin.gradle.plugin)
    }
    
    // buildSrc/src/main/kotlin/kotlin-conventions.gradle.kts
    // Workaround: https://github.com/gradle/gradle/issues/15383
    val libs = the<org.gradle.accessors.dm.LibrariesForLibs>()
    
    plugins {
        kotlin("jvm")
    }
    
    kotlin {
        jvmToolchain {
            languageVersion = JavaLanguageVersion.of(libs.versions.jdk.get())
        }
    }
    
    dependencies {
        testImplementation(kotlin("test"))
    }
    
    tasks.test {
        useJUnitPlatform()
    }
    
    // kotlin-module/build.gradle.kts
    plugins {
        id("kotlin-conventions")
    }

    P.S. Кстати зачем оставлять этот код?

    // Top-level build.gradle.kts
    plugins {
       alias(libs.plugins.android.application) apply false
    }

    apply false имел смысл вместе с заданием версии плагина (для эмуляции поведения как у pluginManagement в Maven), а при использовании version catalog необходимость в нем отпадает.


    1. Rusrst
      24.03.2024 07:45

      Потому что в nio так сделано))) но вроде бы - это не точно, я не очень большой в градл специалист - там есть два способа задания в toml файле для плагинов. И в одном из них это требуется.


  1. GorovoyEG
    24.03.2024 07:45

    Здравствуйте, вы бы хоть ссылку на мою статью оставили, раз уж решили её дополнить историей как добавлялись зависимости раньше)

    Даже скриншоты мои приложили)


    1. popkovden Автор
      24.03.2024 07:45

      Добавил ссылку на статью в описании картинки