Миграция конфигурации сборки с Groovy на Kotlin

Что такое DSL?

DSL (Domain-Specific Language) — это язык программирования, который спроектирован и оптимизирован для решения задач в конкретной области или для определенного класса задач. DSL build.gradle предоставляет разработчикам удобный способ определения настроек проекта и управления ими, используя специфический синтаксис, который Gradle понимает и обрабатывает. Этот DSL позволяет создавать мощные и гибкие сценарии сборки, которые могут быть легко настраиваемы для разных проектов и задач.

С Android Studio Giraffe Kotlin DSL становится новым стандартом для Gradle-скриптов в разработке Android. Когда вы создаете новые проекты, используя встроенные шаблоны IDE, вам будут предоставлены файлы Kotlin DSL вместо файлов Gradle на основе Groovy.

Это предстоящее изменение обеспечило возможность переноса конфигураций Gradle на основе Groovy DSL на Kotlin DSL. Перевод файлов Gradle с Groovy на Kotlin DSL может значительно улучшить рабочий процесс разработки для Android. Особенно если вы уже знакомы с Kotlin. Такой переход на единый знакомый язык не только повышает вашу производительность, но и устраняет необходимость переключаться между двумя языками для выполнения задач разработки и настройки. Надежность и интуитивность Kotlin DSL дает уверенность в создании пользовательских задач Gradle без необходимости прибегать к зачастую нечеткому синтаксису Groovy.

Статья задумана, как руководство, которое поможет вам на этапе перехода на Kotlin DSL. Материал написан с акцентом на проекты Android, но обсуждаемые моменты могут быть применимы и к другим проектам на основе Gradle, таким как приложение Spring Boot.
В первом разделе мы кратко опишем процесс перехода с Groovy на Kotlin DSL в целом. Далее будут перечислены все этапы миграции, через которые прошла наша команда.

Процесс миграции

Обычная структура одномодульных проектов Android в большинстве случаев имеет схожую структуру. Конечно, проект может использовать некоторые дополнительные конфигурационные файлы Gradle, но в целом, процесс миграции одинаков.

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

Обычная одномодульная структура Gradle проекта Android

Обычная одномодульная структура Gradle проекта Android

Исходя из этой структуры, мы можем определить три файла Gradle на основе groovy, которые мы хотим преобразовать в Kotlin DSL:

  • settings.gradle: Файл settings.gradle отвечает за настройку и определение структуры модуля в проекте Android. Он расположен в корневом каталоге проекта и играет решающую роль в организации процесса сборки и управлении им.

  • build.gradle на уровне проекта (Project-level): Файл build.gradle на уровне проекта также находится в корневом каталоге и отвечает за настройку конфигураций для процесса сборки всего проекта. Он определяет глобальные настройки, репозитории и зависимости, которые применяются ко всем модулям в проекте.

  • build.gradle на уровне модуля (Module-level): Внутри каждого отдельного модуля в проекте Android (например, модуля приложения) находится файл build.gradle. Этот файл отвечает за настройку параметров сборки, зависимостей и отвечает за поведение этого конкретного модуля.

Исходя из этой файловой структуры Gradle, структура миграции была следующей:

  1. Перенести файл settings.gradle в файл settings.gradle.kts

  2. Перенести файл build.gradle на уровне проекта в файл build.gradle.kts

  3. Перенести файлы build.gradle на уровне модуля в файлы build.gradle.kts.

Таким образом, у вас будет меньше всего конфликтов. Кроме того, часто помогает закомментировать разделы на каждом этапе миграции, где Kotlin DSL еще не применен, потому что у вас еще не было успешной сборки. Как только проект будет собран с использованием преобразованного файла, подключится поддержка IDE, что позволит быстро устранять ошибки, не пытаясь угадать название свойства Groovy в Kotlin DSL.

Миграционная энциклопедия

Большинство проектных структур очень похожи. Даже при сложных настройках Gradle вы столкнетесь с одинаковыми проблемами в процессе миграции. Ниже приведен список проблем при миграции, на которые мы наткнулись, и способы их решения. В каждом подразделе объясняем, в чем заключается назначение фрагмента Gradle Groovy и как выглядит перенесенный код Kotlin DSL.

Если вы обнаружите какие-либо недостающие части, которые могут быть актуальны для более широкой аудитории, не стесняйтесь оставлять комментарий, и мы добавим их в статью.

Замена одинарных кавычек двойными

Для использования Kotlin DSL нужно убедиться, что используются двойные кавычки (») вместо одинарных кавычек (') для наших строк. Поэтому просмотрите файлы и поищите одинарные кавычки. Вы можете ускорить этот процесс, используя функцию поиска IntelliJ (Mac ⌘ + F; Windows Ctrl + F) или непосредственно функцию замены (Mac ⌘ + R; Windows Ctrl + R).

// build.gradle (Module-level)
implementation 'androidx.core:core-ktx:1.10.1'

При переходе на Kotlin это будет выглядеть так.

// build.gradle.kts (Module-level)
implementation("androidx.core:core-ktx:1.10.1")

Репозиторий плагинов Gradle

Блок repositories используется для определения репозиториев, из которых могут быть взяты зависимости. Для каждого из наших репозиториев мы используем вызовы функций. Однако, объявление репозиториев в Kotlin DSL очень похоже на Groovy. Взгляните на следующее:

// settings.gradle
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

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

// settings.gradle.kts
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

Идентификатор плагина Gradle

Обычно разработчики объявляют идентификатор плагина Gradle, который они хотят использовать на уровне проекта, и заводят его на уровне модуля в виде такого плагина DSL:

// build.gradle (Project-level)
plugins {
    id 'com.android.application' version '8.1.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
    id 'com.android.library' version '8.1.1' apply false
}
// build.gradle (Module-level)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

При переходе на Kotlin это будет выглядеть так:

// build.gradle.kts (Project-level)
plugins {
    id("com.android.application") version "8.1.1" apply false
    id("org.jetbrains.kotlin.android") version "1.8.10" apply false
}

// build.gradle.kts (Module-level)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
}

Плагин Gradle, который все еще использует устаревшее приложение плагина

Если у вас есть плагин Gradle, который использует устаревшее приложение плагина, рекомендуется перейти на плагин DSL. Возьмем, к примеру, плагин Google Play Services Gradle, который существует в проекте уже очень давно:

// build.gradle (Project-level)
buildscript {
    dependencies {
        classpath "com.google.gms:google-services:4.3.15"
    }
}
// build.gradle (Module-level)
apply plugin: "com.google.gms.google-services"

Вместо этого измените его на Plugin DSL:

// build.gradle (Project-level)
plugins {
    id 'com.google.gms:google-services' version '4.3.15' apply false
}
// build.gradle (Module-level)
plugins {
    id 'com.google.gms.google-services'
}

На Kotlin это будет выглядеть так:

// build.gradle.kts (Project-level)
plugins {
    id("com.google.gms:google-services") version "4.3.15" apply false
}
// build.gradle (Module-level)
plugins {
    id("com.google.gms.google-services")
}

Сокращенный идентификатор плагина и функция расширения плагина Kotlin

Некоторые плагины gradle могут быть объявлены сокращенно, например, плагин gradle Kotlin:

// Namespaced Plugin ID
id("org.jetbrains.kotlin.android")
// Shorthand Plugin ID
id("kotlin-android")

Сокращенный

Полностью

kotlin

org.jetbrains.kotlin.jvm

kotlin-android

org.jetbrains.kotlin.android

kotlin-kapt

org.jetbrains.kotlin.kapt

kotlin-parcelize

org.jetbrains.kotlin.plugin.parcelize

А для Kotlin DSL также предусмотрена функция расширения для плагина Gradle.

// build.gradle.kts (Project-level)
plugins {
    kotlin("jvm") version "1.9.10" apply false
    kotlin("android") version "8.1.1" apply false
    kotlin("plugin.parcelize") version "1.9.10" apply false
}

// build.gradle.kts (Module-level)
plugins {
    kotlin("jvm")
    kotlin("android")
    kotlin("plugin.parcelize")
}

Но функции расширения для Android нет.

Миграции extra variables

Extra variables (дополнительные переменные) обычно означают пользовательские переменные, которые могут быть определены и использованы в файле build.gradle. Эти переменные позволяют передавать пользовательские параметры или значения в ваш сценарий сборки, что делает сценарии более гибкими и настраиваемыми. Например, вы можете определить такие переменные в начале файла build.gradle:

// build.gradle (Project-level)
buildscript {
   ext {
       minSdk = 26
       targetSdk = 34
   }
}

// build.gradle (Module-level)
android {
    defaultConfig {
       minSdk rootProject.minSdk
       targetSdk rootProject.targetSdk
    }
}

На Kotlin это будет выглядеть так:

// build.gradle.kts (Project-level)
buildscript {
    extra.apply {
        set("minSdk", 26)
        set("targetSdk", 34)
   }
}
// build.gradle.kts (Module-level)
android {
    defaultConfig {
        minSdk = rootProject.extra["minSdk"] as? Int?
        targetSdk = rootProject.extra["targetSdk"] as? Int?
    }
}

Репозиторий зависимостей в Gradle

Репозиторий зависимостей в Gradle — это место, где Gradle ищет и загружает зависимости (библиотеки и пакеты), которые необходимы для сборки проекта. Gradle поддерживает различные типы репозиториев, такие как локальные репозитории, удаленные репозитории (например, Maven Central), и пользовательские репозитории.

// settings.gradle
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Так что его можно сразу переключить на Kotlin без каких-либо дополнительных усилий:

// settings.gradle.kts
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

Включение модулей Gradle

В ваш settings.gradle включаете свой модуль Gradle. Для проекта с одним модулем —  это только модуль приложения. Если вы переносите многомодульный проект, вам следует перенести все включенные в него модули. Для settings.gradle вы можете преобразовать include следующим образом:

// settings.gradle
include ':app'

На Kotlin это будет выглядеть так:

// settings.gradle.kts
include(":app")

Назначение и конфигурация Android по умолчанию

При настройке параметров в Groovy у нас был синтаксис parameterName value. С помощью Kotlin DSL мы можем просто использовать оператор = между этими двумя значениями, чтобы присвоить значение параметру, если базовый код предоставляет изменяемую переменную.

Для конфигурации Android и defaultConfig результирующий скрипт можно найти в следующем примере фрагмента миграции:

// build.gradle (Module-level)
android {
   namespace 'com.my.project'
   compileSdk 34
   defaultConfig {
       applicationId "com.my.project"
       minSdk 24
       targetSdk 34
       versionCode 1
       versionName "1.0"
       testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
       vectorDrawables {
           useSupportLibrary true
       }
   }
   /* ... */
}

При переходе в Kotlin это выглядит так:

// build.gradle.kts (Module-level)
android {
   namespace = "com.my.project"
   compileSdk = 34
   defaultConfig {
       applicationId = "com.my.project"
       minSdk = 24
       targetSdk = 34
       versionCode = 1
       versionName = "1.0"
       testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
       vectorDrawables {
           useSupportLibrary = true
       }
   }
   /* ... */
}

Определение типов сборки

В Gradle блок buildTypes можно использовать для определения различных конфигураций сборки для проекта Android. Каждый тип сборки представляет собой определенный вариант вашего приложения, такой как debug или release, со своим собственным набором параметров конфигурации.

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

Чтобы создать новый тип сборки с помощью Kotlin DSL аналогично созданию вариантов, мы можем использовать create(..).

// build.gradle (Module-level)
android {
    buildTypes {
        debug { /* ... */ }
        release { 
   	    minifyEnabled true
      proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
   	    signingConfig signingConfigs.release
            /* ... */ 
        }
        googlePlay { /* ... */ }
        galaxyStore { /* ... */ }
        appGallery { /* ... */ }
    }
}

При переходе в Kotlin это выглядит так:

// build.gradle.kts (Module-level)
android {
    buildTypes {
        debug { /* ... */ }
        release {
            isMinifyEnabled = true
 proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
            signingConfig = signingConfigs.getByName("release")
		    /* ... */ 
        }
        create("googlePlay") { /* ... */ }
        create("galaxyStore") { /* ... */ }
        create("appGallery") { /* ... */ }
    }
}

Добавление product flavors

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

Давайте взглянем на пример миграции для определения компонента с именем dev. Обратите внимание, как мы используем функцию .add(..) вместо прямого вызова функции flavorDimensions, поскольку базовым значением являетсяMutableList.

Теперь для создания flavors мы должны использовать синтаксис create("ourFlavorName") вместо прямого объявления имени перед блоком конфигурации.

Кроме того, чтобы добавить базовый URL, поскольку теперь нам приходится использовать двойные кавычки, нужно экранировать внутренние двойные кавычки нашей строки URL с помощью  "\.

// build.gradle (Module-level)
android {
    flavorDimensions = ['default', 'type', 'store']
    buildTypes {
        dev {
            dimension = 'type'
            applicationId 'dev.com.my.project'
   	    buildConfigField 'String', 'BASE_URL', '"https://api.devserver.com"'
            buildConfigField 'Boolean', 'ANALYTICS_ENABLED', 'true'
            /* ... */
        }
        production { 
            dimension "default"
            /* ... */
        }
    }
}

Для создания flavors используем синтаксис create("ourFlavorName") вместо прямого объявления имени перед блоком конфигурации.

// build.gradle.kts (Module-level)
android {
    flavorDimensions += listOf("default", "type", "store")
    buildTypes {
        create("dev") { 
  	    dimension = "app"
            applictionId = "dev.com.my.project"
            buildConfigField("String", "BASE_URL", "\"https://api.devserver.com\"")
            /* ... */
        }
        create("production") { 
            dimension = "default"
            /* ... */
        }
    }
}

Зависимости модулей

В нашем проекте существует несколько способов объявления зависимостей:

  • classpath, используется для объявления зависимостей для самого процесса сборки. Он используется в блоке buildscript в файле build.gradle на уровне проекта.

  • Конфигурация implementation отвечает за объявление зависимостей, которые фактически используются и в нашем коде и во время выполнения.

  • Другим распространенным ключевым словом является platform, которое используется при объявлении зависимостей через спецификацию, из Firebase или Jetpack Compose.

  • При использовании процессоров аннотаций, таких как KAPT или KSP, мы также объявляем соответствующие зависимости в коде, используя ключевые слова kapt и ksp.

  • При включении файлов из каталога, такого как libs, мы можем использовать ключевое слово fileTree.

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

// build.gradle (Module-level)
dependencies {
    implementation fileTree(dir: "libs", include: ["*.jar"])
    implementation project(":library")
    implementation 'androidx.core:core-ktx:1.10.1'
	implementation platform('androidx.compose:compose-bom:2023.03.00')
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    debugImplementation 'androidx.compose.ui:ui-tooling'
    betaDebugImplementation 'com.github.chuckerteam.chucker:library:4.0.0'
	kapt "androidx.room:room-compiler:2.5.2"
 	ksp "androidx.room:room-compiler:2.5.2"
}

В этой форме его можно преобразовать в Kotlin:

// build.gradle.kts (Module-level)
dependencies {
 implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
    implementation(project(":library"))
    implementation("androidx.core:core-ktx:1.10.1")
 	implementation(platform("androidx.compose:compose-bom:2023.03.00"))
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
    debugImplementation("androidx.compose.ui:ui-tooling")
    "betaDebugImplementation"("com.github.chuckerteam.chucker:library:4.0.0")
 	kapt("androidx.room:room-compiler:2.5.2")
    ksp("androidx.room:room-compiler:2.5.2")
}

Настройки подписи

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

// build.gradle (Module-level)
android {
    signingConfigs {
        release {
		     storeFile file("store_file.jks")
            keyAlias properties['key_alias']
            keyPassword properties['key_password']
            storePassword properties['store_password']
        }
    }
}

При переходе в Kotlin это выглядит так:

// build.gradle.kts (Module-level)
android {
    signingConfigs {
        create("release") {
            storeFile = file(properties.getProperty("store_file..jks"))
            storePassword = properties.getProperty("store_password")
            keyAlias = properties.getProperty("key_alias")
            keyPassword = properties.getProperty("key_password")
        }
    }
}

Параметры компиляции

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

// build.gradle (Module-level)
android {
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

При переходе в Kotlin это выглядит так

// build.gradle.kts (Module-level)
android {
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

Блок KotlinOptions

KotlinOptions — это параметр, используемый в файле настроек сборки Gradle для проектов на языке программирования Kotlin. Он позволяет настраивать параметры компиляции и выполнения кода на Kotlin внутри вашего проекта Android.

// build.gradle (Module-level)
android {
    kotlinOptions {
        jvmTarget = "17"
    }
}

Его можно сразу конвертировать в Kotlin, ничего не меняя:

// build.gradle.kts (Module-level)
android {
    kotlinOptions {
        jvmTarget = "17"
    }
}

Features сборки

В функции сборки вы можете включить такие функции, как ViewBinding, RenderScript или Compose.

// build.gradle (Module-level)
android {
    buildFeatures {
        viewBinding true
        dataBinding true
        compose true
    }
}

При переходе в Kotlin это выглядит так:

// build.gradle.kts (Module-level)
android {
    buildFeatures {
        viewBinding = true
        dataBinding = true
        compose = true
    }
}

Исключение ресурсов из вариантов упаковки

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

// build.gradle (Module-level)
android {
    packaging {
        resources.excludes += '/META-INF/{AL2.0,LGPL2.1}'
    }
}

При переходе в Kotlin это выглядит так:

// build.gradle.kts (Module-level)
android {
    packaging {
        resources {
            excludes += setOf("/META-INF/{AL2.0,LGPL2.1}")
        }
    }
}

Настройка параметров тестирования

Для адаптации конфигурации наших модульных тестов мы можем использовать блок UnitTests, включенный в блок testOptions. Здесь единственное, что нам нужно изменить— это включить Android Resources в isIncludeAndroidResources, как вы можете видеть в следующих фрагментах:

// build.gradle (Module-level)
android {
    testOptions {
        unitTests {
            includeAndroidResources true
            returnDefaultValues true
        }
        animationsDisabled true
    }
}

При переходе в Kotlin это выглядит так:

// build.gradle.kts (Module-level)
android {
    testOptions {
        unitTests {
            isIncludeAndroidResources = true
            isReturnDefaultValues = true
        }
        animationsDisabled = true
    }
}

Вывод

Несмотря на доминирование Groovy в настройке скриптов Gradle и его сходство с Java, с которым должны быть знакомы многие разработчики Android или JVM, очень немногие изучают его полностью. Большую часть времени скрипты Gradle полагаются на фрагменты копирования-вставки из соответствующих документов framework, ответов StackOverflow или других источников, что не всегда может обеспечить наиболее оптимальное решение. Таким образом, переход на Kotlin DSL — это не только практическое изменение, но и шаг к улучшению сопровождаемости и читаемости вашего кода.

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

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

© Habrahabr.ru