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.