Durante años, los archivos build.gradle escritos en Groovy han sido el estándar de facto en Gradle. Funcionan, todo el mundo los conoce y hay millones de ejemplos en internet. El problema es que el IDE tiene que adivinar qué métodos existen en cada momento, así que el autocompletado falla con frecuencia, los errores aparecen en tiempo de ejecución y renombrar algo es una aventura.
Gradle lleva tiempo ofreciendo una alternativa: los archivos build.gradle.kts escritos con Kotlin DSL. A partir de Gradle 8.x, el Kotlin DSL es la opción recomendada para proyectos nuevos. Google se sumó en 2023 y lo recomienda oficialmente para Android. Groovy no va a desaparecer, los proyectos existentes seguirán funcionando, pero si empiezas algo nuevo hoy, el KTS es el camino.
Las ventajas concretas del KTS
La diferencia más inmediata es el autocompletado. Con Groovy, IntelliJ tiene que inferir los tipos en tiempo de edición, y falla bastante. Con Kotlin DSL, los tipos son estáticos y el IDE sabe exactamente qué métodos y propiedades existen en cada bloque. Escribes tasks. y te aparece la lista real de tareas disponibles.
Los errores también cambian de categoría. Si escribes mal el nombre de una dependencia o de una tarea en un build.gradle.kts, el build falla durante la fase de compilación del script, antes de intentar ejecutar nada. En Groovy, ese mismo error solo aparece en tiempo de ejecución, cuando ya llevas un rato esperando.
El refactoring es otro punto a favor. Si renombras una función de extensión de Kotlin, los build scripts que la usan se actualizan solos con el refactoring del IDE. Y la documentación oficial de Gradle muestra todos los ejemplos en las dos variantes, Kotlin y Groovy, así que siempre puedes ver cómo se traduce algo.
Estructura básica de un build.gradle.kts
Un archivo de configuración típico para un proyecto Kotlin JVM tiene esta pinta:
plugins {
id("org.jetbrains.kotlin.jvm") version "2.1.0"
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0")
testImplementation(kotlin("test"))
}
kotlin {
jvmToolchain(21)
}
tasks.test {
useJUnitPlatform()
}
El bloque kotlin { jvmToolchain(21) } le dice al compilador qué JDK usar, independientemente del que tengas instalado en el sistema. tasks.test { useJUnitPlatform() } configura la tarea de tests para usar JUnit Platform, que es lo que necesitas con JUnit 5.
La sintaxis es más estricta que Groovy: los strings siempre llevan comillas dobles, las asignaciones van con = y los bloques de configuración de tareas se escriben como tasks.test { ... } en vez del más libre test { ... } de Groovy.
Version catalogs: libs.versions.toml
En un proyecto con un solo módulo esto no duele mucho, pero en un monorepo con diez módulos el problema aparece rápido: las versiones de las dependencias están dispersas por todos los build.gradle.kts y sincronizarlas a mano es una fuente continua de errores.
La solución es el version catalog, que centraliza todas las versiones en un único archivo: gradle/libs.versions.toml. Su estructura es sencilla:
[versions]
kotlin = "2.1.0"
coroutines = "1.8.0"
[libraries]
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
En el build script, en vez de escribir la cadena con el grupo, el artefacto y la versión, usas el acceso tipado:
dependencies {
implementation(libs.kotlinx.coroutines)
}
Con autocompletado. Si escribes libs. el IDE te muestra todo lo que hay definido en el toml. Y cuando quieres actualizar Kotlin, cambias la versión en un único sitio y todos los módulos la recogen.
Plugins de convención: DRY en monorepos
Cuando tienes varios módulos con la misma configuración base de Kotlin, tests y linting, copiar esa configuración en cada build.gradle.kts es tentador pero problemático. Cualquier cambio hay que replicarlo en todos los módulos a mano.
Los plugins de convención resuelven esto. Creas un archivo como buildSrc/src/main/kotlin/my-kotlin-convention.gradle.kts con toda la configuración compartida:
plugins {
id("org.jetbrains.kotlin.jvm")
}
kotlin {
jvmToolchain(21)
}
tasks.test {
useJUnitPlatform()
}
Y en cada módulo que lo necesita, simplemente:
plugins {
id("my-kotlin-convention")
}
Para proyectos grandes, Gradle recomienda usar un directorio build-logic separado en vez de buildSrc. La diferencia es que los cambios en buildSrc invalidan la caché de todos los módulos, mientras que build-logic es un proyecto incluido normal con mejor soporte de caché.
Tareas personalizadas con Kotlin
Registrar una tarea personalizada en KTS es directo:
tasks.register("printVersion") {
doLast {
println(project.version)
}
}
Si la tarea tiene un tipo concreto, lo especificas con el genérico y ganas autocompletado sobre todas las propiedades de ese tipo:
tasks.register<Jar>("fatJar") {
archiveClassifier.set("all")
from(sourceSets.main.get().output)
dependsOn(configurations.runtimeClasspath)
from({
configurations.runtimeClasspath.get().map { if (it.isDirectory) it else zipTree(it) }
})
}
Para configurar todas las tareas de un mismo tipo a la vez, withType con configureEach es la forma correcta de hacerlo sin forzar la configuración anticipada:
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_21
}
}
Gradle y el daemon: tips de rendimiento
El daemon de Gradle es el proceso que queda en memoria entre builds para no tener que reiniciar la JVM cada vez. Está activo por defecto desde Gradle 3.x, así que normalmente no tienes que hacer nada.
Donde sí puedes ganar tiempo es con las cachés. --build-cache guarda los outputs de las tareas y los reutiliza si los inputs no han cambiado, lo que viene muy bien en monorepos donde solo tocas un módulo. Y --configuration-cache guarda el resultado de la fase de configuración del build para no tener que recalcularlo en cada ejecución, algo que en proyectos grandes puede suponer varios segundos por build.
# Build con caché de outputs activada
./gradlew build --build-cache
# Build con caché de configuración (estable desde Gradle 8.x)
./gradlew build --configuration-cache
Si trabajas en un monorepo grande con un equipo, Develocity (antes llamado Gradle Enterprise) añade una caché remota compartida y análisis de builds detallado, lo que puede reducir los tiempos de CI bastante.
Cómo migrar de Groovy a Kotlin DSL
La migración no es complicada, pero hay que tener en cuenta varios cambios de sintaxis.
El primero y más básico: renombrar los archivos. build.gradle pasa a build.gradle.kts y settings.gradle a settings.gradle.kts.
Después, los cambios de sintaxis más habituales:
- Los strings con comillas simples pasan a dobles:
'com.example'se convierte en"com.example". - Las asignaciones de propiedades siempre llevan
=:version = "1.0". - Los bloques de configuración de tareas se escriben como
tasks.test { ... }en vez detest { ... }. - Las llamadas a métodos siempre llevan paréntesis:
implementation("...")en vez deimplementation "...". - El bloque
buildscript { }se simplifica mucho o desaparece del todo si pasas a usar el bloqueplugins { }moderno y los version catalogs.
Para proyectos grandes con mucha configuración en Groovy, lo más cómodo es migrar módulo a módulo. El módulo raíz y buildSrc son los que más configuración suelen tener y los que más tiempo llevan, pero el proceso es mecánico una vez que tienes clara la equivalencia de sintaxis.
Si tienes un proyecto Android, la documentación de Kotlin en Android y el Gradle moderno cubre los plugins específicos de AGP (Android Gradle Plugin) y cómo funcionan con KTS. Y si quieres profundizar en el lenguaje más allá del tooling, Kotlin en el tooling: más allá de las aplicaciones tiene más contexto sobre el uso de Kotlin fuera de Android.
Imagen: Pexels / Markus Spiske
