Crea plug-in Gradle personalizzati per Android KMP

Questa guida spiega come rilevare, interagire e configurare il target Android in un progetto Kotlin Multiplatform (KMP) da un plug-in Gradle personalizzato. Utilizza queste API quando scrivi plug-in di convenzione per la tua build o pubblichi plug-in Gradle per i progetti KMP.

Verifica se un progetto utilizza il plug-in Kotlin Multiplatform

Per evitare errori e assicurarti che il tuo plug-in venga eseguito solo quando è presente KMP, devi verificare se il progetto utilizza il plug-in KMP. La best practice è utilizzare plugins.withId() per reagire all'applicazione del plug-in KMP, anziché verificarlo immediatamente. Questo approccio reattivo impedisce al plug-in di essere vulnerabile all'ordine in cui i plug-in vengono applicati negli script di build dell'utente.

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.
        }
    }
}

Accedi al modello

Il punto di ingresso per tutte le configurazioni Kotlin Multiplatform è l'estensione 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)
        }
    }
}

Reagisci alle destinazioni Kotlin Multiplatform

Utilizza il container targets per configurare in modo reattivo il plug-in per ogni target aggiunto dall'utente.

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
            }
        }
    }
}

Applica la logica specifica per la destinazione

Se il tuo plug-in deve applicare la logica solo a determinati tipi di piattaforme, un approccio comune è controllare la proprietà platformType. Si tratta di un enum che classifica in modo generale il target.

Ad esempio, utilizza questa opzione se il tuo plug-in deve solo distinguere in modo generale (ad esempio, l'esecuzione solo su target simili 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) */ }
                }
            }
        }
    }
}

Dettagli specifici per Android

Sebbene tutte le destinazioni Android abbiano l'indicatore platformType.androidJvm, KMP ha due punti di integrazione distinti a seconda del plug-in Android per Gradle utilizzato: KotlinAndroidTarget per i progetti che utilizzano com.android.library o com.android.application e KotlinMultiplatformAndroidLibraryTarget per i progetti che utilizzano com.android.kotlin.multiplatform.library.

L'API KotlinMultiplatformAndroidLibraryTarget è stata aggiunta in AGP 8.8.0, quindi se i consumer del tuo plug-in utilizzano una versione precedente di AGP, il controllo di target is KotlinMultiplatformAndroidLibraryTarget potrebbe generare un ClassNotFoundException. Per farlo in sicurezza, controlla AndroidPluginVersion.getCurrent() prima di controllare il tipo di destinazione. AndroidPluginVersion.getCurrent() richiede AGP 8.3.1 o versioni successive.

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
                }
            }
        }
    }
}

Accedi all'estensione KMP per Android e alle relative proprietà

Il plug-in interagisce principalmente con l'estensione Kotlin fornita dal plug-in Kotlin Multiplatform e con l'estensione Android fornita da AGP per il target Android KMP. Il blocco android {} all'interno dell'estensione Kotlin in un progetto KMP è rappresentato dall'interfaccia KotlinMultiplatformAndroidLibraryTarget, che estende anche KotlinMultiplatformAndroidLibraryExtension. Ciò significa che puoi accedere alle proprietà DSL specifiche per la destinazione e per Android tramite questo singolo oggetto.

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 differenza di altri plug-in Android (come com.android.library o com.android.application), il plug-in Android KMP non registra la sua estensione DSL principale a livello di progetto. Si trova all'interno della gerarchia di destinazione KMP per garantire che venga applicato solo al target Android specifico definito nella configurazione multipiattaforma.

Gestisci le compilazioni e i set di risorse

Spesso, i plug-in devono funzionare a un livello più granulare rispetto al semplice target, in particolare a livello di compilazione. KotlinMultiplatformAndroidLibraryTarget contiene istanze KotlinMultiplatformAndroidCompilation (ad esempio main, hostTest, deviceTest). Ogni compilazione è associata a set di risorse Kotlin. I plug-in possono interagire con questi elementi per aggiungere origini, dipendenze o configurare attività di compilazione.

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 le compilazioni di test nei plug-in di convenzione

Quando configuri i valori predefiniti per le compilazioni di test (ad esempio targetSdk per i test di instrumentazione) in un plug-in di convenzione, evita di utilizzare metodi di attivazione come withDeviceTest { } o withHostTest { }. La chiamata di questi metodi attiva la creazione delle varianti di test Android corrispondenti e delle compilazioni per ogni modulo che applica il plug-in della convenzione, il che potrebbe non essere adatto. Inoltre, questi metodi non possono essere chiamati una seconda volta in un modulo specifico per perfezionare le impostazioni, perché ciò genera un errore che indica che la compilazione è già stata creata.

Utilizza invece un blocco configureEach reattivo nel container delle compilation. In questo modo puoi fornire configurazioni predefinite che si applicano solo se e quando un modulo attiva esplicitamente la compilazione del test:

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) }
                    }
                }
        }
    }
}

Questo pattern garantisce che il plug-in di convenzione rimanga lazy e consente ai singoli moduli di chiamare withDeviceTest { } per attivare e personalizzare ulteriormente i test senza entrare in conflitto con i valori predefiniti.

Interagisci con l'API Variant

Per le attività che richiedono una configurazione in fase avanzata, l'accesso agli artefatti (come manifest o bytecode) o la possibilità di attivare o disattivare componenti specifici, devi utilizzare l'API Android Variant. Nei progetti KMP, l'estensione è di tipo KotlinMultiplatformAndroidComponentsExtension.

L'estensione viene registrata a livello di progetto quando viene applicato il plug-in Android KMP.

Utilizza beforeVariants per controllare la creazione di varianti o dei relativi componenti di test nidificati (hostTests e deviceTests). Questo è il punto corretto per disattivare i test in modo programmatico o modificare i valori delle proprietà 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 }
            }
        }
    }
}

Utilizza onVariants per accedere all'oggetto della variante finale (KotlinMultiplatformAndroidVariant). Qui puoi ispezionare le proprietà risolte o registrare le trasformazioni su artefatti come il manifest unito o le classi della libreria.

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)
            }
        }
    }
}