Kotlin guide done. (20.10.26)

This commit is contained in:
2026-10-20 12:54:25 +03:00
parent da3ad9dd1a
commit 6853ada348
81 changed files with 30112 additions and 0 deletions
@@ -0,0 +1,383 @@
---
title: Gradle и Kotlin DSL
description: build.gradle.kts, конфигурации зависимостей, version catalog, таски и настройка компилятора
tags:
- kotlin
- tooling
- gradle
---
Gradle - стандарт сборки Kotlin-проектов. Kotlin DSL (`build.gradle.kts`) - это обычный Kotlin-код, поэтому IDE даёт автодополнение, переход к определению и проверку типов, чего не было в Groovy DSL.
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
application
}
repositories { mavenCentral() }
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
testImplementation(kotlin("test"))
}
kotlin { jvmToolchain(21) }
application { mainClass.set("com.example.MainKt") }
tasks.test { useJUnitPlatform() }
```
Это полный рабочий файл. `kotlin("jvm")` - сокращение для `id("org.jetbrains.kotlin.jvm")`, `kotlin("test")` - для `org.jetbrains.kotlin:kotlin-test` версии, совпадающей с плагином.
## Файлы сборки
| Файл | Назначение |
|---|---|
| `settings.gradle.kts` | имя проекта, список модулей, репозитории плагинов |
| `build.gradle.kts` | конфигурация модуля |
| `gradle.properties` | флаги Gradle, память JVM |
| `gradle/libs.versions.toml` | version catalog |
| `gradlew` / `gradlew.bat` | wrapper - фиксирует версию Gradle |
| `gradle/wrapper/gradle-wrapper.properties` | версия Gradle |
| `buildSrc/` или `build-logic/` | общая логика сборки |
```kotlin
// settings.gradle.kts
rootProject.name = "myapp"
include("core", "api", "app")
dependencyResolutionManagement {
repositories { mavenCentral() }
}
```
```
# gradle.properties
org.gradle.jvmargs=-Xmx4g
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
kotlin.daemon.jvmargs=-Xmx2g
kotlin.incremental=true
```
Четыре флага дают основной выигрыш в скорости: параллельная сборка модулей, кеш результатов, кеш конфигурации и инкрементальная компиляция Kotlin.
## Конфигурации зависимостей
```kotlin
// build.gradle.kts
dependencies {
implementation("io.ktor:ktor-client-core:3.0.0") // нужна при компиляции и в рантайме
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2") // + видна потребителям модуля
compileOnly("org.jetbrains:annotations:26.0.0") // только при компиляции
runtimeOnly("ch.qos.logback:logback-classic:1.5.18") // только в рантайме
testImplementation(kotlin("test"))
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.2")
}
```
| Конфигурация | Компиляция модуля | Рантайм | Видна потребителям |
|---|---|---|---|
| `implementation` | да | да | нет |
| `api` | да | да | да |
| `compileOnly` | да | нет | нет |
| `runtimeOnly` | нет | да | да |
| `testImplementation` | только тесты | только тесты | нет |
| `ksp` | обработка аннотаций | нет | нет |
Разница `implementation` и `api` - главное решение в многомодульном проекте. `implementation` скрывает транзитивную зависимость: при её изменении перекомпилируется только ваш модуль. `api` экспортирует тип в публичное API модуля - без неё потребитель не увидит типы из зависимости в ваших сигнатурах.
Правило: `api` только тогда, когда тип из зависимости появляется в публичных сигнатурах. Иначе `implementation` - он быстрее пересобирается и не навязывает зависимость.
Для библиотеки нужен плагин `java-library`:
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
}
dependencies {
api("org.jetbrains.kotlinx:kotlinx-datetime:0.6.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
```
Обратные кавычки вокруг `java-library` нужны из-за дефиса: это идентификатор Kotlin, а не строка.
## Version catalog
Версии в одном месте, типизированный доступ из DSL:
```
# gradle/libs.versions.toml
[versions]
kotlin = "2.4.0"
coroutines = "1.11.0"
ktor = "3.0.0"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
[bundles]
async = ["coroutines-core", "ktor-client"]
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version = "2.4.0-2.0.0" }
```
```kotlin
// build.gradle.kts
plugins {
alias(libs.plugins.kotlin.jvm)
}
repositories { mavenCentral() }
dependencies {
implementation(libs.bundles.async)
implementation(libs.coroutines.core)
testImplementation(libs.kotlin.test)
testImplementation(libs.coroutines.test)
}
```
Дефис в TOML превращается в точку в DSL: `coroutines-core` → `libs.coroutines.core`. Каталог подхватывается автоматически по пути `gradle/libs.versions.toml`, настройка не нужна.
Преимущество перед `val kotlinVersion = "2.4.0"` в начале файла: каталог работает во всех модулях и виден Dependabot/Renovate для автообновлений.
## Настройка компилятора
```kotlin
// build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
kotlin {
jvmToolchain(21)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_21)
languageVersion.set(KotlinVersion.KOTLIN_2_4)
apiVersion.set(KotlinVersion.KOTLIN_2_2)
allWarningsAsErrors.set(true)
progressiveMode.set(true)
freeCompilerArgs.addAll(
"-Xjsr305=strict",
"-opt-in=kotlin.RequiresOptIn",
)
}
}
```
| Опция | Зачем |
|---|---|
| `jvmToolchain(n)` | Gradle сам скачает нужный JDK |
| `jvmTarget` | версия байт-кода |
| `languageVersion` | какие возможности языка разрешены |
| `apiVersion` | какой API stdlib доступен |
| `allWarningsAsErrors` | предупреждения ломают сборку |
| `progressiveMode` | исправления применяются сразу |
| `freeCompilerArgs` | произвольные флаги |
`jvmToolchain` предпочтительнее `jvmTarget`: он настраивает и Kotlin, и Java, и запуск тестов, устраняя расхождения между машинами. Подробнее про флаги: [kotlinc](kotlinc.md)
## Таски
```kotlin
// build.gradle.kts
tasks.register("generateVersion") {
val outputDir = layout.buildDirectory.dir("generated/version")
val version = project.version.toString()
outputs.dir(outputDir)
doLast {
val file = outputDir.get().file("Version.kt").asFile
file.parentFile.mkdirs()
file.writeText("const val APP_VERSION = \"$version\"\n")
}
}
tasks.named("compileKotlin") {
dependsOn("generateVersion")
}
tasks.test {
useJUnitPlatform()
maxParallelForks = Runtime.getRuntime().availableProcessors() / 2
testLogging {
events("passed", "failed", "skipped")
}
}
```
Важная деталь: значения проекта (`project.version`) читаются **вне** `doLast`, в фазе конфигурации. Обращение к `project` внутри `doLast` несовместимо с configuration cache и даёт предупреждение или ошибку.
Частые таски:
| Таск | Что делает |
|---|---|
| `build` | компиляция + тесты + сборка артефактов |
| `assemble` | только артефакты, без тестов |
| `test` | тесты |
| `clean` | удалить `build/` |
| `run` | запуск (плагин `application`) |
| `dependencies` | дерево зависимостей |
| `tasks --all` | список всех тасков |
| `kotlinDslAccessorsReport` | доступные аксессоры DSL |
```
./gradlew build --no-daemon
./gradlew test --tests "com.example.UserServiceTest"
./gradlew dependencies --configuration runtimeClasspath
./gradlew build --scan # отчёт о производительности
./gradlew --profile build # локальный профиль сборки
```
## Многомодульный проект
```
myapp/
├── settings.gradle.kts
├── build.gradle.kts # общая конфигурация
├── gradle/libs.versions.toml
├── core/build.gradle.kts
├── api/build.gradle.kts
└── app/build.gradle.kts
```
```kotlin
// корневой build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0" apply false
}
subprojects {
apply(plugin = "org.jetbrains.kotlin.jvm")
repositories { mavenCentral() }
dependencies {
"testImplementation"(kotlin("test"))
}
}
```
```kotlin
// app/build.gradle.kts
dependencies {
implementation(project(":core"))
implementation(project(":api"))
}
```
`apply false` в корне означает «добавить плагин в classpath, но не применять к корневому проекту» - корневой модуль обычно не содержит кода.
Строковая форма `"testImplementation"(...)` нужна внутри `subprojects`: типизированный аксессор там недоступен, потому что плагин ещё не применён на момент конфигурации.
Для серьёзных проектов вместо `subprojects` используют convention plugins в `buildSrc/` или `build-logic/` - они работают с configuration cache и дают нормальную типизацию. Подробнее: [Структура проекта](Структура%20проекта.md)
## Kotlin DSL против Groovy DSL
| Groovy (`build.gradle`) | Kotlin (`build.gradle.kts`) |
|---|---|
| `implementation 'group:art:1.0'` | `implementation("group:art:1.0")` |
| `ext.version = '1.0'` | `val version = "1.0"` |
| `task hello { ... }` | `tasks.register("hello") { ... }` |
| `apply plugin: 'java'` | `plugins { java }` |
| `sourceCompatibility = 1.8` | `java.sourceCompatibility = JavaVersion.VERSION_1_8` |
| динамический, без проверок | типизированный, с автодополнением |
| быстрее первая конфигурация | требует компиляции скрипта |
Kotlin DSL - рекомендуемый вариант с Gradle 8. Основная цена - первая сборка медленнее (скрипт компилируется), зато ошибки в конфигурации видны в IDE, а не при запуске.
## Публикация
```kotlin
// build.gradle.kts
plugins {
kotlin("jvm") version "2.4.0"
`java-library`
`maven-publish`
}
group = "com.example"
version = "1.0.0"
java {
withSourcesJar()
withJavadocJar()
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
pom {
name.set("My Library")
description.set("Описание библиотеки")
licenses {
license {
name.set("Apache-2.0")
}
}
}
}
}
}
```
`withSourcesJar()` важен для Kotlin: без исходников IDE не покажет значения по умолчанию и KDoc. `./gradlew publishToMavenLocal` публикует в `~/.m2` для локальной проверки.
> [!info] Отличия от Java
> | Java-мир | Kotlin-мир |
> |---|---|
> | Maven `pom.xml` или Gradle Groovy | Gradle Kotlin DSL |
> | `<dependency><scope>compile</scope>` | `implementation` / `api` |
> | `maven-compiler-plugin` | `jvmToolchain` / `compilerOptions` |
> | `<properties>` для версий | `libs.versions.toml` |
> | annotation processor path | конфигурация `ksp` |
> | XML, декларативно | код, императивно |
>
> **Kotlin DSL - код, а не данные.** В Maven `pom.xml` - декларативный документ: инструменты его читают и модифицируют надёжно. `build.gradle.kts` - программа: в ней можно написать цикл, условие, вызов функции. Это даёт гибкость (генерация тасков, вычисляемые версии) и создаёт проблему - статически понять, что делает сборка, нельзя. Отсюда configuration cache: Gradle вынужден кешировать результат исполнения скрипта.
>
> **`implementation` против `compile` в Maven - это про скорость сборки.** В Maven `compile`-scope экспортирует зависимость транзитивно всегда: потребитель видит всё дерево. Gradle разделил на `api` и `implementation`, и это принципиально: изменение `implementation`-зависимости не вызывает перекомпиляцию потребителей. На больших проектах разница в минутах.
>
> **Toolchain решает проблему, которой в Maven нет в таком виде.** Maven полагается на JDK, под которым запущен, плюс `maven.compiler.release`. В Kotlin нужно согласовать три вещи: JDK компиляции, `jvmTarget` Kotlin и `targetCompatibility` Java - и расхождение даёт ошибку «Inconsistent JVM-target compatibility». `jvmToolchain(n)` настраивает всё сразу и скачивает JDK при необходимости.
>
> **Version catalog - ответ на `<dependencyManagement>`.** В Maven версии централизуются через BOM и `<properties>`, в Gradle долго не было стандарта (каждый писал свои `ext`-переменные). `libs.versions.toml` типизирован, виден в IDE и понятен инструментам автообновления.
>
> Сравнить: [JVM](../../Программирование%20на%20Java/JVM/JVM.md)
> [!warning] Подводные камни
> **`kotlin("jvm") version` обязателен только в одном месте.** Указание версии в нескольких модулях даёт ошибку «Plugin request for plugin already on the classpath must not include a version». В подмодулях - `kotlin("jvm")` без версии, в корне - с версией и `apply false`.
>
> **Не указывайте stdlib вручную.** `implementation(kotlin("stdlib"))` избыточен: плагин добавляет его сам, причём правильной версии. Явное указание другой версии даёт конфликт.
>
> **`jvmTarget` без toolchain ломается при смене JDK.** Локально JDK 21, в CI 17 - сборка упадёт. `jvmToolchain(21)` делает результат воспроизводимым.
>
> **Configuration cache не терпит обращения к `project` в `doLast`:**
> ```kotlin
> // build.gradle.kts
> // неправильно: project читается во время выполнения
> tasks.register("bad") {
> doLast { println(project.version) }
> }
>
> // правильно: значение захвачено в фазе конфигурации
> tasks.register("good") {
> val v = project.version.toString()
> doLast { println(v) }
> }
> ```
> Первый вариант даёт ошибку «invocation of 'Task.project' at execution time is unsupported».
>
> **`api` вместо `implementation` по привычке замедляет сборку.** Транзитивно экспортированная зависимость заставляет перекомпилировать всех потребителей при её изменении. На большом проекте это разница между инкрементальной сборкой за 5 секунд и полной за 2 минуты.
>
> **`gradlew` нужно коммитить, включая `gradle-wrapper.jar`.** Без него сборка на другой машине использует локальный Gradle другой версии. Некоторые политики безопасности запрещают JAR в репозитории - тогда нужен `gradle/wrapper/gradle-wrapper.properties` плюс проверка контрольной суммы через `validation-action` в CI.
>
> **Дефис в имени из каталога превращается в точку.** `coroutines-test` в TOML - это `libs.coroutines.test` в DSL. Попытка написать `libs.coroutines-test` не скомпилируется, а `libs.coroutinesTest` не найдётся.
>
> **Плагины компилятора должны совпадать по версии с Kotlin.** `kotlin("plugin.serialization") version "2.3.0"` при основном плагине 2.4.0 даёт ошибку несовместимости. Держите обе версии в `[versions] kotlin`.
>
> **Версия KSP привязана к версии Kotlin.** Формат `2.4.0-2.0.0`: первая часть - версия Kotlin, вторая - KSP. Обновление Kotlin требует одновременного обновления KSP. Подробнее: [KSP](KSP.md)
>
> **`./gradlew clean build` почти всегда лишнее.** `clean` отключает инкрементальную компиляцию и кеш, превращая 10-секундную сборку в минутную. Используйте только при реальных проблемах с кешем.
>
> **Нехватка памяти демона проявляется как медленная сборка, а не как ошибка.** При `-Xmx` меньше нужного JVM тратит время на GC. Для среднего проекта `org.gradle.jvmargs=-Xmx4g` и `kotlin.daemon.jvmargs=-Xmx2g` - разумный минимум.
>
> **Устаревшая `apiVersion` вместе с `allWarningsAsErrors` роняет сборку.** В Kotlin 2.4 значение `KOTLIN_2_1` даёт «API version 2.1 is deprecated and its support will be removed in a future version», а `-Werror` превращает предупреждение в ошибку. Поддерживается примерно три предыдущих версии - при обновлении Kotlin проверяйте, что указанная `apiVersion` ещё не устарела.
>
> **`tasks.create` устарел в пользу `tasks.register`.** `create` конфигурирует таск немедленно, при каждой сборке; `register` - лениво, только если таск нужен. На проектах с сотнями тасков разница заметна.
>
> **Groovy- и Kotlin-скрипты нельзя смешивать в одном файле.** Переход на Kotlin DSL - это переписывание всего `build.gradle` в `build.gradle.kts`; наличие обоих файлов в модуле даёт ошибку.
Далее: [Структура проекта](Структура%20проекта.md)