wandres.dev
GRADLE PARA ANDROID · KTS, catálogos y variantes

Kotlin DSL: build.gradle.kts, el bloque android y la migración desde Groovy

El fichero de compilación deja de ser un script dinámico y pasa a ser código Kotlin compilado y verificado por el compilador. Esta lección explica qué gana y qué cuesta ese cambio: cómo funciona un DSL de tipos seguros construido sobre lambdas con receptor, por qué el IDE puede autocompletar cada propiedad del bloque android cuando antes no podía, y qué significa que un error de escritura pase de ser un fallo silencioso en tiempo de ejecución a un error de compilación. Recorre después la anatomía completa de un build.gradle.kts de Android moderno —plugins, android, dependencies— y cierra con una estrategia de migración desde Groovy que no exige detener el desarrollo: fichero a fichero, con las reglas de traducción mecánica y las trampas conocidas de los delegados, las cadenas y las llamadas a métodos.

⏱ 20 min

Durante una década los ficheros de compilación de Android se escribieron en Groovy, un lenguaje dinámico donde cualquier símbolo podía significar cualquier cosa y el IDE no tenía forma honesta de ayudarte. Escribías complieSdk en lugar de compileSdk y el build no fallaba: Groovy interpretaba la línea como una llamada a un método desconocido que el DSL absorbía en silencio, y descubrías el problema tres pantallas más abajo con un error incomprensible. El DSL de Kotlin cambia la premisa de raíz: tu build.gradle.kts es un fichero Kotlin que se compila antes de ejecutarse, contra un modelo de tipos generado a partir de los plugins que has aplicado. Lo que ganas no es sintaxis más bonita —de hecho es algo más verbosa— sino que el compilador y el IDE por fin sepan de qué estás hablando.

🎯 Al terminar esta lección sabrás
  • Entender el DSL de Gradle como lambdas con receptor y saber leer sus bloques anidados.
  • Reconocer la anatomía completa de un build.gradle.kts de Android moderno.
  • Configurar el bloque android con criterio: SDK, compilación, opciones de Kotlin y buildFeatures.
  • Migrar desde Groovy de forma incremental aplicando reglas de traducción y evitando las trampas conocidas.

Qué es realmente un DSL de tipos seguros

Un DSL en Gradle no es magia sintáctica: es una función que recibe una lambda con receptor. Cuando escribes un bloque android seguido de llaves, estás llamando a una función llamada android cuyo único argumento es una lambda cuyo this implícito es un objeto de configuración. Dentro de esa lambda, cada línea que parece una declaración es en realidad una asignación a una propiedad o una llamada a un método de ese receptor. Esa es toda la mecánica, y entenderla convierte el fichero de un conjunto de fórmulas memorizadas en código que puedes razonar.

// La firma real, simplificada: una funcion de extension sobre Project que
// recibe una lambda con receptor de tipo ApplicationExtension
fun Project.android(configurar: ApplicationExtension.() -> Unit)

// Por eso dentro del bloque el this implicito es la propia extension,
// y cada linea es una asignacion o una llamada sobre ese receptor
android {
    // this: ApplicationExtension
    compileSdk = 36
    defaultConfig {
        // this: ApplicationDefaultConfig
        minSdk = 26
    }
}

La diferencia con Groovy no es estética. En Groovy, android era una llamada a un método interceptado dinámicamente y las propiedades se resolvían por nombre en tiempo de ejecución; nada podía verificarse antes. En Kotlin, android es una función de extensión real cuyo tipo receptor depende del plugin aplicado —ApplicationExtension para un módulo de aplicación, LibraryExtension para una biblioteca— y por eso el autocompletado funciona, la navegación al código fuente funciona y un nombre mal escrito es un error de compilación antes de que se ejecute nada.

🛡️

Errores en compilación

Un nombre de propiedad equivocado, un tipo incorrecto o un bloque que no existe fallan al compilar el script, con la línea exacta señalada, en lugar de producir comportamiento silencioso o un error críptico más adelante.

🧭

El IDE por fin sabe

Autocompletado real, ver la documentación al vuelo, saltar a la definición de una propiedad y renombrar con seguridad. Todo eso exige tipos, y por eso en Groovy nunca funcionó del todo.

📚

Un solo lenguaje

Tu app está en Kotlin y tu build también. Las mismas reglas de sintaxis, las mismas colecciones, las mismas funciones de extensión. Se acabó cambiar de gramática al abrir un fichero distinto.

🐢

El coste: la primera vez

El script hay que compilarlo, y esa compilación se nota en la primera sincronización tras un cambio. Gradle la cachea con agresividad, pero es el peaje honesto del modelo y conviene conocerlo.

Anatomía de un módulo de aplicación

Un fichero de compilación de Android moderno tiene tres bloques mayores y un orden que no es arbitrario: primero se declaran los plugins, porque son los que introducen las extensiones; después se configura la extensión que esos plugins han aportado; por último se declaran las dependencias.

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.ejemplo.app"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.ejemplo.app"
        minSdk = 26
        targetSdk = 36
        versionCode = 1
        versionName = "1.0.0"
    }

    buildFeatures {
        compose = true
        buildConfig = true
    }

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

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
    }
}

dependencies {
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.compose.material3)
}

Cuatro detalles merecen comentario. namespace es el paquete que usan R y BuildConfig, y desde hace varias versiones vive aquí y no en el manifiesto. applicationId es una cosa distinta: es la identidad de la app en el dispositivo y en Google Play, y puede diferir del namespace —de hecho lo hará en cuanto uses sabores—. compileSdk es la versión de la API contra la que compilas y debe ser la más reciente estable; minSdk y targetSdk son promesas de comportamiento en tiempo de ejecución, no de compilación. Y buildFeatures es un interruptor de generación de código: cada bandera que activas añade tareas al grafo, así que activar solo lo que usas es una decisión de rendimiento, no de estilo.

ℹ️
Los bloques de Kotlin salieron del bloque android

En versiones antiguas el objetivo de la JVM para Kotlin se configuraba dentro de android con kotlinOptions. Esa vía está obsoleta: hoy se configura en el bloque kotlin de nivel superior mediante compilerOptions, que es el DSL unificado del plugin de Kotlin para todas las plataformas. Si te encuentras kotlinOptions en un proyecto, no es un error todavía, pero sí una señal de que el fichero lleva tiempo sin revisarse.

El orden de las capas y quién configura qué

Conviene tener presente de dónde sale cada bloque, porque la confusión habitual —creer que android es una palabra clave de Gradle— desaparece en cuanto ves la cadena de causalidad.

flowchart TD
S[settings punto gradle punto kts declara repositorios y modulos] --> P[bloque plugins aplica el plugin de aplicacion de Android]
P --> E[el plugin registra la extension ApplicationExtension]
E --> A[el bloque android configura esa extension]
A --> T[el plugin traduce la configuracion en tareas y variantes]
P --> D[el plugin tambien registra configuraciones como implementation]
D --> Q[el bloque dependencies usa esas configuraciones]
style S fill:#89b4fa,color:#11111b
style E fill:#cba6f7,color:#11111b
style T fill:#a6e3a1,color:#11111b

Lo que este diagrama deja claro es que no puedes configurar android sin haber aplicado antes el plugin, ni usar implementation sin que algún plugin haya registrado esa configuración. El orden de los bloques dentro del fichero importa por esta razón concreta y no por otra: plugins va primero porque es quien crea el vocabulario que el resto del fichero usa. Y también explica por qué settings.gradle.kts es un fichero aparte con sus propias reglas: se evalúa en la fase de inicialización, antes de que exista ningún proyecto que configurar, y por eso allí se declaran los repositorios y los módulos participantes y no dependencias.

Migrar desde Groovy sin detener el mundo

La migración no exige un día de parada. Gradle acepta que unos módulos usen build.gradle y otros build.gradle.kts en el mismo proyecto, así que la estrategia sensata es empezar por settings.gradle, seguir por los módulos hoja —los que menos lógica tienen— y dejar el raíz y el de la aplicación para el final, cuando ya domines las reglas.

Las traducciones mecánicas son cuatro y cubren la inmensa mayoría del trabajo. Las cadenas pasan de comillas simples a comillas dobles. Las asignaciones a propiedades exigen el signo igual explícito, donde Groovy lo omitía. Las llamadas a métodos exigen paréntesis, también donde Groovy los omitía; la trampa está en distinguir cuál es cuál, porque en Groovy ambas cosas se escribían igual. Y def desaparece en favor de val.

// Groovy                     ->  Kotlin DSL
// compileSdk 36              ->  compileSdk = 36            (propiedad)
// implementation 'g:a:1.0'   ->  implementation("g:a:1.0")  (metodo)
// def x = 1                  ->  val x = 1

// Tareas: registro perezoso y tipado
tasks.register<Copy>("copiarNotas") {
    from(layout.projectDirectory.dir("notas"))
    into(layout.buildDirectory.dir("notas"))
}

// Propiedades del proyecto y del entorno, sin romper la cache de configuracion
val esCi: Boolean = providers.environmentVariable("CI").isPresent
val sabor: String = providers.gradleProperty("saborPorDefecto").getOrElse("dev")

Hay tres trampas que causan casi todos los tropiezos. La primera es la resolución de tareas: en Groovy escribías el nombre de una tarea y aparecía; en Kotlin necesitas tasks.named o el operador de delegación, y conviene usar la variante tipada para poder configurar propiedades específicas. La segunda son los ext de Groovy para versiones compartidas, que no tienen traducción idiomática y no deben tenerla: su sustituto correcto es el catálogo de versiones que estudiarás en la siguiente lección. La tercera es leer propiedades del sistema o del entorno con las APIs de Java, que rompe la caché de configuración; usa siempre providers como en el ejemplo anterior.

💡
Que el compilador te enseñe la API

La ventaja práctica más infravalorada del DSL de Kotlin es que puedes explorar la configuración sin documentación. Dentro del bloque android, invoca el autocompletado y verás las propiedades reales de la extensión, con sus tipos y su documentación. Salta a la definición de defaultConfig y estarás leyendo la interfaz del plugin de Android. El fichero de compilación deja de ser un lugar donde copias fórmulas de foros y pasa a ser código navegable como cualquier otro.

Un fichero de configuracion tambien merece un compilador

Hay una idea grande escondida detrás de un cambio que parece cosmético. Durante años la industria aceptó una asimetría absurda: exigíamos tipos, pruebas y revisión de código para el software que enviábamos a los usuarios, y a la vez tolerábamos que la definición de cómo se construía ese software viviera en un lenguaje dinámico sin verificación, sin autocompletado y sin navegación, donde un error de escritura se manifestaba como un comportamiento raro tres capas más abajo. El fichero de compilación se trataba como un accesorio, no como código. Y sin embargo es el único artefacto del proyecto del que dependen absolutamente todos los demás: si está mal, no hay app. El DSL de Kotlin es la corrección de esa asimetría, y su enseñanza trasciende Gradle. Todo lo que produce un artefacto es código, y todo el código merece las mismas garantías: el fichero de infraestructura, el de la canalización de integración continua, el del despliegue, el esquema de la base de datos. Cada vez que una de esas piezas se escribe en un lenguaje sin tipos ni verificación, estás decidiendo que sus errores se descubran en producción en lugar de en tu editor, y esa decisión casi nunca se toma de forma consciente: se hereda. Fíjate además en el mecanismo, porque es el mismo en todas partes: la seguridad de tipos no viene de escribir más, viene de que exista un modelo explícito —aquí, las extensiones que aporta cada plugin— contra el que un compilador pueda contrastar lo que escribes. Sin modelo no hay verificación posible, y sin verificación la única defensa es la disciplina humana, que es exactamente la defensa que falla a las tres de la mañana antes de una entrega. Migrar a build.gradle.kts cuesta una tarde. Lo que compras con esa tarde es que tu build empiece a fallar en el sitio correcto.

⚔️ Traduce y verifica
  1. Coge un build.gradle en Groovy de cualquier proyecto y tradúcelo a build.gradle.kts aplicando las cuatro reglas mecánicas. Anota cada línea donde tuviste que decidir si era propiedad o método.
  2. Escribe mal a propósito el nombre de una propiedad dentro del bloque android y observa el error. Compara mentalmente con lo que habría ocurrido en Groovy.
  3. Registra una tarea de tipo Copy con tasks.register y configúrala después con tasks.named desde otro punto del fichero. Explica por qué esta forma es preferible a tasks.create.
  4. Desactiva buildConfig en buildFeatures, compila y observa qué se rompe. Justifica por qué cada bandera de generación de código tiene un coste en el grafo de tareas.
  5. Sustituye cualquier lectura directa de variables de entorno por providers.environmentVariable y argumenta qué garantía ganas de cara a la caché de configuración.