Compila complementos personalizados de Gradle para KMP de Android

En esta guía, se explica cómo detectar, interactuar y configurar el destino de Android en un proyecto de Kotlin Multiplatform (KMP) desde un complemento de Gradle personalizado. Usa estas APIs cuando escribas complementos de convenciones para tu compilación o publiques complementos de Gradle para proyectos de KMP.

Cómo verificar si un proyecto usa el complemento de Kotlin Multiplatform

Para evitar errores y asegurarte de que tu complemento solo se ejecute cuando KMP esté presente, debes verificar si el proyecto usa el complemento de KMP. Se recomienda usar plugins.withId() para reaccionar a la aplicación del complemento de KMP, en lugar de verificarlo de inmediato. Este enfoque reactivo evita que tu complemento sea frágil en el orden en que se aplican los complementos en las secuencias de comandos de compilación del usuario.

import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            // The KMP plugin is applied, you can now configure your KMP integration.
        }
    }
}

Accede al modelo

El punto de entrada para todas las configuraciones de Kotlin Multiplatform es la extensión KotlinMultiplatformExtension.

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
        }
    }
}

Cómo reaccionar a los destinos de Kotlin Multiplatform

Usa el contenedor targets para configurar de forma reactiva tu complemento para cada destino que agregue el usuario.

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                // 'target' is an instance of KotlinTarget
                val targetName = target.name // for example, "android", "iosX64", "jvm"
                val platformType = target.platformType // for example, androidJvm, jvm, native, js
            }
        }
    }
}

Aplica lógica específica para el destino

Si tu complemento necesita aplicar lógica solo a ciertos tipos de plataformas, un enfoque común es verificar la propiedad platformType. Esta es una enumeración que categoriza el objetivo de forma general.

Por ejemplo, usa esto si tu complemento solo necesita diferenciar de forma general (como ejecutarse solo en destinos similares a JVM):

import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                when (target.platformType) {
                    KotlinPlatformType.jvm -> { /* Standard JVM */ }
                    KotlinPlatformType.androidJvm -> { /* Android */ }
                    KotlinPlatformType.js -> { /* JavaScript */ }
                    KotlinPlatformType.native -> { /* Any Native (iOS, Linux, Windows, etc.) */ }
                    KotlinPlatformType.wasm -> { /* WebAssembly */ }
                    KotlinPlatformType.common -> { /* Metadata target (rarely needs direct plugin interaction) */ }
                }
            }
        }
    }
}

Detalles específicos de Android

Si bien todos los destinos de Android tienen el indicador platformType.androidJvm, KMP tiene dos puntos de integración distintos según el complemento de Android para Gradle que se use: KotlinAndroidTarget para los proyectos que usan com.android.library o com.android.application, y KotlinMultiplatformAndroidLibraryTarget para los proyectos que usan com.android.kotlin.multiplatform.library.

La API de KotlinMultiplatformAndroidLibraryTarget se agregó en AGP 8.8.0, por lo que, si los consumidores de tu complemento ejecutan una versión anterior de AGP, la verificación de target is KotlinMultiplatformAndroidLibraryTarget podría generar un ClassNotFoundException. Para que esto sea seguro, verifica AndroidPluginVersion.getCurrent() antes de verificar el tipo de destino. AndroidPluginVersion.getCurrent() requiere AGP 8.3.1 o una versión posterior.

import com.android.build.api.AndroidPluginVersion
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("org.jetbrains.kotlin.multiplatform") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                if (target is KotlinAndroidTarget) {
                    // Old kmp android integration using com.android.library or com.android.application
                } else if (
                    // Check platformType first, so AGP classes load only for Android targets
                    target.platformType == KotlinPlatformType.androidJvm &&
                    AndroidPluginVersion.getCurrent() >= AndroidPluginVersion(8, 8) &&
                    target is KotlinMultiplatformAndroidLibraryTarget
                ) {
                    // New kmp android integration using com.android.kotlin.multiplatform.library
                }
            }
        }
    }
}

Cómo acceder a la extensión de KMP para Android y sus propiedades

Tu complemento interactúa principalmente con la extensión de Kotlin que proporciona el complemento de Kotlin Multiplatform y la extensión de Android que proporciona el AGP para el destino de Android de KMP. El bloque android {} dentro de la extensión de Kotlin en un proyecto de KMP se representa con la interfaz KotlinMultiplatformAndroidLibraryTarget, que también extiende KotlinMultiplatformAndroidLibraryExtension. Esto significa que puedes acceder a las propiedades de DSL específicas del destino y de Android a través de este único objeto.

import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)

            // Access the Android target, which also serves as the Android-specific DSL extension
            kmpExtension.targets.withType(KotlinMultiplatformAndroidLibraryTarget::class.java).configureEach { androidTarget ->

                // You can now access properties and methods from both
                // KotlinMultiplatformAndroidLibraryTarget and KotlinMultiplatformAndroidLibraryExtension
                androidTarget.compileSdk = 34
                androidTarget.namespace = "com.example.myplugin.library"
                androidTarget.withJava() // enable Java sources
            }
        }
    }
}

A diferencia de otros complementos para Android (como com.android.library o com.android.application), el complemento para Android de KMP no registra su extensión de DSL principal a nivel del proyecto. Se encuentra dentro de la jerarquía de destinos de KMP para garantizar que solo se aplique al destino de Android específico definido en tu configuración multiplataforma.

Cómo controlar las compilaciones y los conjuntos de orígenes

A menudo, los complementos deben funcionar en un nivel más detallado que solo el objetivo, específicamente en el nivel de compilación. KotlinMultiplatformAndroidLibraryTarget contiene instancias de KotlinMultiplatformAndroidCompilation (por ejemplo, main, hostTest y deviceTest). Cada compilación se asocia con conjuntos de orígenes de Kotlin. Los complementos pueden interactuar con estos para agregar fuentes, dependencias o configurar tareas de compilación.

import com.android.build.api.dsl.KotlinMultiplatformAndroidCompilation
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val kmpExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.configureEach { target ->
                target.compilations.configureEach { compilation ->
                    // standard compilations are usually 'main' and 'test'
                    // android target has 'main', 'hostTest', 'deviceTest'
                    val compilationName = compilation.name

                    // Access the default source set for this compilation
                    val defaultSourceSet = compilation.defaultSourceSet

                    // Access the Android-specific compilation DSL
                    if (compilation is KotlinMultiplatformAndroidCompilation) {

                    }

                    // Access and configure the Kotlin compilation task
                    compilation.compileTaskProvider.configure { compileTask ->

                    }
                }
            }
        }
    }
}

Configura compilaciones de prueba en complementos de convenciones

Cuando configures valores predeterminados para compilaciones de prueba (como targetSdk para pruebas de instrumentación) en un complemento de convenciones, evita usar métodos de habilitación, como withDeviceTest { } o withHostTest { }. Llamar a estos métodos de forma inmediata activa la creación de las variantes de prueba de Android y las compilaciones correspondientes para cada módulo que aplica el complemento de convención, lo que podría no ser adecuado. Tampoco se puede llamar a estos métodos por segunda vez en un módulo específico para definir mejor la configuración, ya que, si lo haces, se genera un error que indica que ya se creó la compilación.

En su lugar, usa un bloque configureEach reactivo en el contenedor de compilaciones. Esto te permite proporcionar parámetros de configuración predeterminados que solo se aplican si un módulo habilita de forma explícita la compilación de prueba:

import com.android.build.api.dsl.KotlinMultiplatformAndroidDeviceTestCompilation
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val kmpExtension =
                project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            kmpExtension.targets.withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
                .configureEach { androidTarget ->
                    androidTarget.compilations.withType(
                        KotlinMultiplatformAndroidDeviceTestCompilation::class.java
                    ).configureEach {
                        it.targetSdk { version = release(34) }
                    }
                }
        }
    }
}

Este patrón garantiza que tu complemento de convención siga siendo diferido y permite que los módulos individuales llamen a withDeviceTest { } para habilitar y personalizar aún más sus pruebas sin entrar en conflicto con los valores predeterminados.

Interactúa con la API de Variant

Para las tareas que requieren una configuración de etapa tardía, acceso a artefactos (como manifiestos o código de bytes) o la capacidad de habilitar o inhabilitar componentes específicos, debes usar la API de Android Variant. En los proyectos de KMP, la extensión es de tipo KotlinMultiplatformAndroidComponentsExtension.

La extensión se registra a nivel del proyecto cuando se aplica el complemento para Android de KMP.

Usa beforeVariants para controlar la creación de variantes o sus componentes de prueba anidados (hostTests y deviceTests). Este es el lugar correcto para inhabilitar pruebas de manera programática o cambiar los valores de las propiedades de DSL.

import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val androidComponents = project.extensions.findByType(KotlinMultiplatformAndroidComponentsExtension::class.java)
            androidComponents?.beforeVariants { variantBuilder ->
                // Disable all tests for this module
                variantBuilder.hostTests.values.forEach { it.enable = false }
                variantBuilder.deviceTests.values.forEach { it.enable = false }
            }
        }
    }
}

Usa onVariants para acceder al objeto de variante final (KotlinMultiplatformAndroidVariant). Aquí es donde puedes inspeccionar las propiedades resueltas o registrar transformaciones en artefactos, como el manifiesto combinado o las clases de biblioteca.

import com.android.build.api.variant.KotlinMultiplatformAndroidComponentsExtension
import org.gradle.api.Plugin
import org.gradle.api.Project

class MyPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val androidComponents = project.extensions.findByType(KotlinMultiplatformAndroidComponentsExtension::class.java)
            androidComponents?.onVariants { variant ->
                // 'variant' is a KotlinMultiplatformAndroidVariant
                val variantName = variant.name

                // Access the artifacts API
                val manifest = variant.artifacts.get(com.android.build.api.artifact.SingleArtifact.MERGED_MANIFEST)
            }
        }
    }
}