Compila complementos personalizados de Gradle para KMP de Android

En este documento, se proporciona una guía para los autores de complementos sobre cómo detectar, interactuar y configurar correctamente la configuración de Kotlin Multiplatform (KMP), con un enfoque específico en la integración con los destinos de Android dentro de un proyecto de KMP. A medida que KMP sigue evolucionando, es fundamental comprender los hooks y las APIs adecuados, como los tipos KotlinMultiplatformExtension y KotlinTarget, y las interfaces de integración específicas de Android, para compilar herramientas sólidas y preparadas para el futuro que funcionen sin problemas en todas las plataformas definidas en un proyecto multiplataforma.

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 al orden en que se aplican los complementos en los 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 del destino

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

Por ejemplo, usa esta opción si tu complemento solo necesita diferenciar de forma general (por ejemplo, ejecutarse solo en destinos similares a la 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 or Android */ }
                    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 Gradle para Android que se use: KotlinAndroidTarget para proyectos que usan com.android.library o com.android.application, y KotlinMultiplatformAndroidLibraryTarget para 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, verificar target is KotlinMultiplatformAndroidLibraryTarget podría generar un ClassNotFoundException. Para que esto sea seguro, verifica AndroidPluginVersion.getCurrent() antes de verificar el tipo de destino. Ten en cuenta que AndroidPluginVersion.getCurrent() requiere AGP 7.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.mpp.KotlinAndroidTarget

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 ->
                if (target is KotlinAndroidTarget) {
                    // Old kmp android integration using com.android.library or com.android.application
                }
                if (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 de Android y a sus propiedades

Tu complemento interactuará principalmente con la extensión de Kotlin que proporciona el complemento de Kotlin Multiplatform y la extensión de Android que proporciona 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 del 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 de Android (como com.android.library o com.android.application), el complemento de Android de KMP no registra su extensión principal del DSL a nivel del proyecto. Se encuentra dentro de la jerarquía de destino de KMP para garantizar que solo se aplique al destino específico de Android definido en tu configuración multiplataforma.

Cómo controlar las compilaciones y los conjuntos de fuentes

A menudo, los complementos deben funcionar a un nivel más detallado que solo el objetivo; específicamente, deben funcionar a nivel de compilación. El KotlinMultiplatformAndroidLibraryTarget contiene instancias de KotlinMultiplatformAndroidCompilation (por ejemplo, main, hostTest, deviceTest). Cada compilación está asociada con conjuntos de fuentes 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 basados en convenciones

Cuando configures valores predeterminados para las compilaciones de prueba (como targetSdk para las pruebas instrumentadas) en un complemento de convenciones, debes evitar usar métodos de habilitación como withDeviceTest { } o withHostTest { }. Llamar a estos métodos de forma anticipada activa la creación de las variantes de prueba y las compilaciones de Android correspondientes para cada módulo que aplica el complemento de convenciones, lo que podría no ser adecuado. Además, estos métodos no se pueden llamar una segunda vez en un módulo específico para ajustar la configuración, ya que, si lo haces, se arrojará un error que indica que ya se creó la compilación.

En cambio, te recomendamos que uses un bloque configureEach reactivo en el contenedor de compilaciones. Esto te permite proporcionar configuraciones predeterminadas que solo se aplican si un módulo habilita explícitamente la compilación de pruebas:

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 {
                        targetSdk { version = release(34) }
                    }
                }
        }
    }
}

Este patrón garantiza que el complemento de convenciones permanezca inactivo 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 configuración en etapas avanzadas, 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 forma programática o cambiar los valores de las propiedades del 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í 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.variant.SingleArtifact.MERGED_MANIFEST)
            }
        }
    }
}