El problema sin convention plugins
Con 10 módulos, cada uno tiene su propio build.gradle. Sin convention plugins, todos se parecen así:
// :feature:checkout/build.gradle — SIN convention plugins
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}
android {
compileSdk = 35
defaultConfig { minSdk = 24 }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions { jvmTarget = "17" }
}
dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
implementation(libs.androidx.core.ktx)
// ... más dependencias
}
// :feature:productos/build.gradle — prácticamente igual
// :feature:auth/build.gradle — prácticamente igual
// :core:ui/build.gradle — prácticamente igual
// ...
// 10 archivos con las mismas 30 líneas. Un cambio de compileSdk → 10 ediciones.
buildSrc vs includeBuild — cuál elegir
# buildSrc
# → Carpeta especial que Gradle compila automáticamente antes del build
# → Más simple de configurar inicialmente
# → Desventaja: cualquier cambio en buildSrc invalida la caché de TODOS los módulos
# → Recomendado para proyectos pequeños o iniciar la migración
# includeBuild (composite build)
# → El build-logic es un proyecto Gradle separado incluido en el build
# → Cambios en build-logic solo invalidan los módulos que lo usan
# → Más rápido en proyectos grandes
# → Recomendado para proyectos con más de 15-20 módulos
# → Es lo que usa la arquitectura de referencia de Google (now-in-android)
Estructura del build-logic con includeBuild
miapp/
├── build-logic/ # proyecto Gradle separado
│ ├── settings.gradle.kts
│ ├── build.gradle.kts
│ └── convention/
│ ├── build.gradle.kts # dependencias del build-logic
│ └── src/main/kotlin/
│ ├── AndroidLibraryConventionPlugin.kt
│ ├── AndroidFeatureConventionPlugin.kt
│ ├── AndroidHiltConventionPlugin.kt
│ └── KotlinLibraryConventionPlugin.kt
├── app/
├── feature/
├── core/
└── settings.gradle.kts # raíz
# settings.gradle.kts (raíz) — incluir el build-logic:
pluginManagement {
includeBuild("build-logic")
repositories { /* ... */ }
}
include(":app", ":feature:checkout", ":core:domain", /* ... */)
El primer convention plugin — Android Library base
// build-logic/convention/src/main/kotlin/AndroidLibraryConventionPlugin.kt
class AndroidLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
// Aplicar los plugins necesarios para una librería Android
with(pluginManager) {
apply("com.android.library")
apply("org.jetbrains.kotlin.android")
}
// Configurar opciones comunes
extensions.configure<LibraryExtension> {
compileSdk = 35
defaultConfig {
minSdk = 24
// No se define targetSdk en librería — lo define :app
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
// Deshabilitar builds que no necesitamos en librerías
buildTypes {
release {
isMinifyEnabled = false // las librerías no minifican
}
}
}
// Dependencias comunes a todas las librerías Android
dependencies {
"implementation"(libs.findLibrary("androidx.core.ktx").get())
"implementation"(libs.findLibrary("kotlinx.coroutines.android").get())
}
}
}
}
// Registrar el plugin en build-logic/convention/build.gradle.kts:
gradlePlugin {
plugins {
register("androidLibrary") {
id = "miapp.android.library"
implementationClass = "AndroidLibraryConventionPlugin"
}
}
}
Plugin específico para features con Hilt
// AndroidFeatureConventionPlugin.kt
class AndroidFeatureConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
// Reusar el plugin base de librería Android
pluginManager.apply("miapp.android.library")
// Agregar Hilt a todos los feature modules
with(pluginManager) {
apply("com.google.dagger.hilt.android")
apply("com.google.devtools.ksp")
}
dependencies {
"implementation"(libs.findLibrary("hilt.android").get())
"ksp"(libs.findLibrary("hilt.compiler").get())
// Dependencias que todos los features necesitan:
"implementation"(project(":core:domain"))
"implementation"(project(":core:ui"))
// Tests:
"testImplementation"(project(":core:testing"))
}
}
}
}
// Registrar:
register("androidFeature") {
id = "miapp.android.feature"
implementationClass = "AndroidFeatureConventionPlugin"
}
Usar los convention plugins en cada módulo
// :feature:checkout/build.gradle.kts — ANTES (30 líneas)
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("com.google.dagger.hilt.android")
id("com.google.devtools.ksp")
}
android {
compileSdk = 35
// ... 20 líneas más
}
dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
// ... más dependencias
}
// :feature:checkout/build.gradle.kts — DESPUÉS (5 líneas)
plugins {
alias(libs.plugins.miapp.android.feature)
}
dependencies {
// Solo lo que es específico de checkout
implementation(libs.stripe.android)
}
// :core:domain/build.gradle.kts — módulo Kotlin puro, también simple
plugins {
alias(libs.plugins.miapp.kotlin.library)
}
dependencies {
api(libs.kotlinx.coroutines.core) // api para exponer el tipo a los consumidores
}
Un cambio de compileSdk → un solo lugarCon convention plugins, actualizar de compileSdk = 35 a compileSdk = 36 es una sola línea en AndroidLibraryConventionPlugin.kt. Todos los módulos la usan automáticamente. Sin plugins, es un cambio en cada uno de los 10+ build.gradle.