Builds multi-módulo: dividir, conectar y compartir configuración
Compón un build de Gradle con módulos independientes y deja de copiar y pegar configuración.
Un build de Gradle es un árbol de proyectos. El proyecto raÃz rara vez contiene código de producción; actúa como contenedor que une los subproyectos (módulos). Todo módulo que Gradle deba conocer se declara en `settings.gradle.kts` con `include(...)`. Una llamada como `include(":app", ":core")` le indica a Gradle que el build se compone de dos subproyectos, `:app` y `:core`, cada uno en su propio directorio con su propio `build.gradle.kts`. Los dos puntos iniciales son un separador de ruta desde la raÃz: `:core` significa el directorio `core` en la raÃz del build, y `:feature:login` corresponderÃa a `feature/login`. El archivo `settings.gradle.kts` es la única fuente de verdad sobre qué módulos existen: si un directorio no se incluye con `include`, Gradle simplemente lo ignora.
Una vez que existen los módulos, los conectas con dependencias de proyecto. Dentro de `app/build.gradle.kts` escribes `implementation(project(":core"))` (o el accesor con tipos `implementation(projects.core)` una vez que activas los accesores de proyecto con tipos mediante `enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")` en `settings.gradle.kts`). Esto expresa que `:app` depende de la salida compilada y de la API de `:core`. Gradle compila `:core` primero y luego `:app`, de forma incremental: si cambias un archivo en `:app`, `:core` no se vuelve a compilar. Elegir `api` frente a `implementation` importa: `api(project(":core"))` expone las dependencias propias de `:core` a los consumidores de `:app`, mientras que `implementation` las mantiene internas, lo que acelera la compilación y evita acoplamientos accidentales.
Lo difÃcil de un build multi-módulo no es dividir el código, sino mantener la configuración consistente. No quieres redefinir (y dejar divergir lentamente) la versión de Kotlin, el target de la JVM, el framework de pruebas y las opciones del compilador en cada módulo. Gradle ofrece dos mecanismos. `buildSrc` es un directorio especial que Gradle compila antes que el resto del build; todo lo que pongas ahà (funciones auxiliares, constantes y especialmente script plugins precompilados) queda disponible para cada `build.gradle.kts`. El enfoque más moderno y recomendado son los convention plugins: pequeños plugins precompilados, normalmente escritos en `buildSrc` o en un build incluido `build-logic`, que agrupan una porción reutilizable de configuración detrás de un único id de plugin.
Un convention plugin es simplemente un archivo `*.gradle.kts` ubicado en `buildSrc/src/main/kotlin/`. Un archivo llamado `kotlin-library-conventions.gradle.kts` se convierte automáticamente en un plugin que puedes aplicar con `id("kotlin-library-conventions")`. Dentro de él aplicas el plugin de Kotlin, configuras el toolchain de la JVM, añades las dependencias de prueba estándar y configuras las opciones del compilador, una sola vez. Cada módulo de librerÃa se reduce entonces a `plugins { id("kotlin-library-conventions") }`, y su archivo de build se encoge hasta dejar solo las dependencias propias de ese módulo. Asà es como los grandes proyectos se mantienen manejables: las decisiones compartidas viven en un plugin, no duplicadas en decenas de archivos.
Los catálogos de versiones (`gradle/libs.versions.toml`) son el tercer pilar y combinan de forma natural con los builds multi-módulo. El catálogo centraliza cada coordenada y versión de dependencia en un único archivo TOML, exponiéndolas como accesores con tipos como `libs.kotlinx.coroutines` en todos los módulos (y, con algo de configuración, también dentro de tus convention plugins). Esto significa que una sola edición actualiza una librerÃa en todas partes, tu IDE autocompleta las dependencias y no hay riesgo de que `:app` y `:core` traigan en silencio dos versiones distintas de la misma librerÃa. Combina el catálogo con los convention plugins y el script de build de un módulo se vuelve casi puramente una declaración de qué es y con qué habla.
¿Cuándo conviene dividir realmente? Crea un nuevo módulo cuando tengas un lÃmite claro que valga la pena imponer: una capa de dominio que no debe depender del framework web, código compartido por varios puntos de entrada (un servidor de API y una CLI), o una pieza lenta de compilar que quieres cachear y paralelizar de forma independiente. Los módulos te dan builds incrementales y paralelos más rápidos, una arquitectura que se puede imponer (el compilador rechaza dependencias prohibidas) y mejor cacheo de build. Pero cada módulo añade sobrecarga: configuración, un grafo de dependencias que razonar y un costo de navegación entre módulos. No dividas de antemano una app pequeña en una docena de módulos el primer dÃa; deja que los lÃmites reales y el dolor real de tiempo de build vayan extrayendo módulos de un monolito, no la especulación.
// settings.gradle.kts — declares which modules make up the buildrootProject.name = "my-app"dependencyResolutionManagement {// shared repositories used to resolve dependencies in every modulerepositories { mavenCentral() }}// each path maps to a directory: :core -> core/, :app -> app/include(":app", ":core")
settings.gradle.kts is the single source of truth for the module list and the shared repositories.
// app/build.gradle.kts — a thin module file: conventions + its own depsplugins {// all shared Kotlin/JVM/test config comes from this one pluginid("kotlin-library-conventions")}dependencies {// depend on the :core module's compiled output and APIimplementation(project(":core"))// type-safe accessor sourced from gradle/libs.versions.tomlimplementation(libs.kotlinx.coroutines.core)}
implementation(project(":core")) wires modules together; the convention plugin removes boilerplate.
// buildSrc/src/main/kotlin/kotlin-library-conventions.gradle.kts// A convention plugin: configure shared decisions exactly once.plugins {kotlin("jvm")}kotlin {jvmToolchain(21) // every module that applies this plugin gets JDK 21}tasks.test {useJUnitPlatform() // standardized test runner for all modules}
Apply this with id("kotlin-library-conventions") in any module to inherit the shared setup.
🧠Comprueba tu comprensión
0/1 · 0/1 answered1. In a multi-module Gradle build, what is the purpose of a convention plugin placed in buildSrc (e.g. kotlin-library-conventions.gradle.kts)?