Привет, Хабр! Я Дима Котиков, ведущий android-разработчик в Т-Банке. Работаю в команде приложения Долями. Разработкой под Android начал увлекаться в 2020 году, а потом хобби переросло в работу. Люблю разбираться в технологиях, разрабатывать под Android и KMP и латте на фундучном молоке 🙂
Я расскажу о том, как облегчить работу с Gradle с использованием Gradle Convention Plugins. Всю информацию я разбил на серию статей для удобства. Они будут полезны всем, кто пользуется Gradle в качестве сборщика проектов. В первой части поговорим о проблеме с build.gradle
-файлами и сделаем начальную настройку для написания Gradle Convention Plugins.
build.gradle.kts — монстр
Каждый android-разработчик так или иначе сталкивается со сборщиком проектов Gradle и видит в своих модулях файлы build.gradle
или build.gradle.kts
. Дальше примеры будут на базе build.gradle.kts
-файлов для kotlin-multiplatform-проекта, но в целом информация применима и к build.gradle
.
Скрытый текст
import org.jetbrains.compose.ExperimentalComposeLibrary import org.jetbrains.compose.desktop.application.dsl.TargetFormat import com.android.build.api.dsl.ManagedVirtualDevice import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree plugins { alias(libs.plugins.multiplatform) alias(libs.plugins.compose.compiler) alias(libs.plugins.compose) alias(libs.plugins.android.application) alias(libs.plugins.kotlinx.serialization) } kotlin { androidTarget { compilations.all { compileTaskProvider { compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) freeCompilerArgs.add("-Xjdk-release=${JavaVersion.VERSION_1_8}") } } } //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html @OptIn(ExperimentalKotlinGradlePluginApi::class) instrumentedTestVariant { sourceSetTree.set(KotlinSourceSetTree.test) dependencies { debugImplementation(libs.androidx.testManifest) implementation(libs.androidx.junit4) } } } jvm() listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { it.binaries.framework { baseName = "ComposeApp" isStatic = true } } sourceSets { commonMain.dependencies { implementation(project(":shared-uikit")) implementation(compose.runtime) implementation(compose.foundation) implementation(compose.material3) implementation(compose.components.resources) implementation(compose.components.uiToolingPreview) implementation(libs.coil) implementation(libs.coil.network.ktor) implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.core) implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.datetime) } commonTest.dependencies { implementation(kotlin("test")) @OptIn(ExperimentalComposeLibrary::class) implementation(compose.uiTest) implementation(libs.kotlinx.coroutines.test) } androidMain.dependencies { implementation(compose.uiTooling) implementation(libs.androidx.activityCompose) implementation(libs.kotlinx.coroutines.android) implementation(libs.ktor.client.okhttp) } jvmMain.dependencies { implementation(compose.desktop.currentOs) implementation(libs.kotlinx.coroutines.swing) implementation(libs.ktor.client.okhttp) } iosMain.dependencies { implementation(libs.ktor.client.darwin) } } } android { namespace = "io.github.dmitriy1892.gradleconventionpuginssample" compileSdk = 34 defaultConfig { minSdk = 24 targetSdk = 34 applicationId = "io.github.dmitriy1892.gradleconventionpuginssample.androidApp" versionCode = 1 versionName = "1.0.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } sourceSets["main"].apply { manifest.srcFile("src/androidMain/AndroidManifest.xml") res.srcDirs("src/androidMain/res") } //https://developer.android.com/studio/test/gradle-managed-devices @Suppress("UnstableApiUsage") testOptions { managedDevices.devices { maybeCreate<ManagedVirtualDevice>("pixel5").apply { device = "Pixel 5" apiLevel = 34 systemImageSource = "aosp" } } } compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } buildFeatures { //enables a Compose tooling support in the AndroidStudio compose = true } } compose.desktop { application { mainClass = "MainKt" nativeDistributions { targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) packageName = "io.github.dmitriy1892.gradleconventionpuginssample.desktopApp" packageVersion = "1.0.0" } } }
Код выглядит монструозно. Если проект многомодульный, в каждом модуле содержится build.gradle.kts
-файл примерно с таким же наполнением.
Все бы ничего, но есть моменты, которые могут доставить проблем и заставить вручную пройти по всем модулям, если держать конфигурации модулей как в примере. Так может случиться, когда нужно:
1. Поднять версии библиотек, minSdk/targetSdk/compileSdk или Java;
2. Поднять версию Gradle-wrapper или Android Gradle Plugin, что в случае изменения API классов или функций gradle-конфигураций может повлечь необходимость изменений в файлах build.gradle.kts
каждого модуля;
3. Обработать deprecation-ы разного рода конфигураций в новых версиях Gradle Plugin, AGP, KMP и тому подобное:
4. Копипастить все из build.gradle.kts
при необходимости создать модуль вручную и нудно настраивать под конкретный модуль.
Хочется избавиться от этого списка проблем, повторяющихся частей, соблюсти DRY. На помощь может прийти Gradle Convention Plugins.
Gradle Convention Plugins
Gradle Convention Plugins — это инструмент, позволяющий переиспользовать конфигурации сборки, уменьшить Boilerplate в build.gradle.kts
-файлах и проще управлять изменениями при смене версий библиотек, плагинов и тому подобного. А еще он упрощает поддержку конфигураций сборки.
Для использования механизма нужно создать модуль для Convention Plugins, выделить базовые конфигурации, применить их при написании Convention Plugins, зарегистрировать плагины в build.gradle.kts
нашего модуля с плагинами и использовать в проекте.
На каждом моменте остановимся подробнее. Используем подход с построением Composite Builds — это подход, в котором наши плагины и конфигурации сборки лежат в отдельных независимых модулях и подключаются к процессу сборки основного проекта. При этом модули с плагинами собираются раньше, чтобы предоставить плагины для сборки нашего проекта.
Пример того, как можно писать Convention Plugins, есть в моем проекте на GitHub, начальный код в ветке initial
.
Нам нужно подготовиться, чтобы написать Convention Plugins:
-
Создать gradle-модуль, не зависящий от каких-либо других в проекте, — в нем мы будем размещать наши будущие Convention Plugins.
-
Подключить модуль с плагинами в корневой проект так, чтобы он собирался до сборки остальных модулей проекта. Результат сборки — скомпилированные Convention Plugins, будем использовать в модулях основного проекта.
-
Сконфигурировать
build.gradle.kts
иsettings.gradle.kts
этого модуля правильным образом, чтобы Gradle понимал, что это модуль с плагинами.
По сути, мы будем использовать механизм композитной сборки Gradle — у нас будет независимый модуль, который по системе понятий Gradle будет независимым проектом. После сборки такого проекта его скомпилированные артефакты могут быть использованы другим gradle-проектом, его подмодулями и подпроектами, который подключил наш независимый проект как композит.
Начинаем настройку для написания Gradle Convention Plugins:
1. Создадим папку convention-plugins
в проекте, в ней создадим папку base
, в которой будут содержаться базовые конфигурации и extension-функции для наших плагинов. Добавим пустые gradle-файлы base-модуля.
2. Зайдем в settings.gradle.kts
и пропишем наш модуль как includeBuild
:
Подключение модуля Convention Plugin отличается от подключения обычного модуля функцией includeBuild
, а путь прописывается не через двоеточия, а через slash (`/`).
Вот тут как раз и будет работать механизм композитной сборки: сначала соберется модуль или проект, указанный в includeBuild
, а потом — основной проект.
3. Конфигурируем build.gradle.kts
и settings.gradle.kts
для нашего модуля с плагинами.
Конфигурируем settings.gradle.kts
— пропишем необходимые репозитории с плагинами и зависимостями, из которых будем запрашивать нужные нам зависимости для использования в наших будущих Convention Plugin-ах:
import java.net.URI pluginManagement { repositories { google() gradlePluginPortal() mavenCentral() maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } dependencyResolutionManagement { repositories { google() mavenCentral() maven { url = URI("https://androidx.dev/storage/compose-compiler/repository/") } } versionCatalogs { create("libs") { from(files("../../gradle/libs.versions.toml")) } } } rootProject.name = "base"
Мы указали репозитории для плагинов и зависимостей, прописали создание Version Catalog и указали путь до libs.versions.toml
-файла.
Добавляем в libs.versions.toml
-файл переменную с версией Java и зависимости нужных нам плагинов и конфигурируем build.gradle.kts
модуля convention-plugins/base
:
Дублирую зависимости для тех, кто идет по шагам при чтении статьи:
# Plugins for composite build gradleplugin-android = { module = "com.android.tools.build:gradle", version.ref = "agp" } gradleplugin-compose = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose" } gradleplugin-composeCompiler = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "compose" } gradleplugin-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
Конфигурируем build.gradle.kts
модуля convention-plugins/base
:
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `kotlin-dsl` } group = "io.github.dmitriy1892.conventionplugins.base" dependencies { implementation(libs.gradleplugin.android) implementation(libs.gradleplugin.compose) implementation(libs.gradleplugin.composeCompiler) implementation(libs.gradleplugin.kotlin) // Workaround for version catalog working inside precompiled scripts // Issue - https://github.com/gradle/gradle/issues/15383 implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) } private val projectJavaVersion: JavaVersion = JavaVersion.toVersion(libs.versions.java.get()) java { sourceCompatibility = projectJavaVersion targetCompatibility = projectJavaVersion } tasks.withType<KotlinCompile>().configureEach { compilerOptions.jvmTarget.set(JvmTarget.fromTarget(projectJavaVersion.toString())) }
В коде мы сделали следующее:
-
В блоке
plugins
подключили плагинkotlin-dsl
, чтобы использовать Gradle Kotlin Dsl для написания наших Convention Plugins; -
В блоке
dependencies
добавили плагины, которые будем использовать в наших будущих Convention Plugins. Сразу отмечу, что добавлять плагины через блокplugins
не получится: такова специфика модуля с Convention Plugin; -
В блоке
dependencies
добавили Workaround, чтобы в наших плагинах были доступны Version Catalogs. Пока нет возможности по-другому использовать Version Catalogs, есть issue. -
Задали версию Java и kotlin-компилятора.
Создаем первый пустой Convention Plugin и подключаем его к корневому build.gradle.kts
нашего проекта. Это нужно для того, чтобы работали extension-функции в build.gradle.kts
-файлах модулей корневого проекта, в который мы подключили наш модуль с Convention Plugins.
Предварительную настройку закончили — переходим к созданию базовых Сonvention pugins в .gradle.kts
-файлах.
О том, как создавать плагины и переиспользуемые части в .gradle.kts
-файлах и Kotlin extension-функций для упрощения написания плагинов — в следующей статье. Не переключайтесь!
ссылка на оригинал статьи https://habr.com/ru/articles/843648/
Добавить комментарий