Crea plug-in Gradle personalizzati per Android KMP

Questo documento fornisce una guida per gli autori di plug-in su come rilevare, interagire e configurare correttamente la configurazione di Kotlin Multiplatform (KMP), con un focus specifico sull'integrazione con i target Android all'interno di un progetto KMP. Man mano che KMP continua a evolversi, comprendere gli hook e le API corretti, come i tipi KotlinMultiplatformExtension e KotlinTarget e le interfacce di integrazione specifiche di Android, è essenziale per creare strumenti solidi e a prova di futuro che funzionino perfettamente su tutte le piattaforme definite in un progetto multipiattaforma.

Verificare se un progetto utilizza il plug-in Kotlin Multiplatform

Per evitare errori e assicurarti che il plug-in venga eseguito solo quando è presente KMP, devi verificare se il progetto utilizza il plug-in KMP. La best practice consiste nell'utilizzare plugins.withId() per reagire all'applicazione del plug-in KMP, anziché controllarlo immediatamente. Questo approccio reattivo impedisce al plug-in di essere fragile rispetto 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.
        }
    }
}

Accedere al modello

Il punto di ingresso per tutte le configurazioni di 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)
        }
    }
}

Reagire ai target multipiattaforma Kotlin

Utilizza il contenitore 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 una 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'enumerazione che classifica in modo generico il target.

Ad esempio, utilizzalo se il plug-in deve solo differenziare in modo generico (ad esempio, eseguire solo su target simili alla 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) */ }
                }
            }
        }
    }
}

Dettagli specifici per Android

Mentre tutti i target Android hanno l'indicatore platformType.androidJvm, KMP ha due punti di integrazione distinti a seconda del plug-in Android 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 consumatori del tuo plug-in utilizzano una versione precedente di AGP, il controllo di target is KotlinMultiplatformAndroidLibraryTarget potrebbe generare un ClassNotFoundException. Per rendere questa operazione sicura, controlla AndroidPluginVersion.getCurrent() prima di controllare il tipo di target. Tieni presente che AndroidPluginVersion.getCurrent() richiede AGP 7.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.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
                }
            }
        }
    }
}

Accedere all'estensione KMP di Android e alle relative proprietà

Il plug-in interagirà 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 il target 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 KMP Android non registra la sua estensione DSL principale a livello di progetto. Si trova all'interno della gerarchia dei target KMP per assicurarsi che venga applicato solo al target Android specifico definito nella configurazione multipiattaforma.

Gestire le compilation e i set di fonti

Spesso, i plug-in devono operare a un livello più granulare rispetto al solo target, in particolare devono operare a livello di compilazione. KotlinMultiplatformAndroidLibraryTarget contiene KotlinMultiplatformAndroidCompilation istanze (ad esempio main, hostTest, deviceTest). Ogni compilazione è associata a set di origini Kotlin. I plug-in possono interagire con questi 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 ->

                    }
                }
            }
        }
    }
}

Configurare le compilazioni di test nei plug-in delle convenzioni

Quando configuri i valori predefiniti per le compilazioni di test (ad esempio targetSdk per i test strumentati) in un plug-in di convenzione, devi evitare di utilizzare metodi di attivazione come withDeviceTest { } o withHostTest { }. La chiamata di questi metodi attiva immediatamente la creazione delle varianti di test e delle compilazioni Android corrispondenti per ogni modulo che applica il plug-in di 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é in questo modo verrà generato un errore che indica che la compilazione è già stata creata.

Ti consigliamo invece di utilizzare un blocco configureEach reattivo nel contenitore delle compilation. In questo modo puoi fornire configurazioni predefinite che vengono applicate 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 {
                        targetSdk { version = release(34) }
                    }
                }
        }
    }
}

Questo pattern assicura che il plug-in delle convenzioni rimanga pigro e consenta ai singoli moduli di chiamare withDeviceTest { } per attivare e personalizzare ulteriormente i test senza entrare in conflitto con i valori predefiniti.

Interagire con l'API Variant

Per le attività che richiedono la 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 KMP Android.

Utilizza beforeVariants per controllare la creazione di varianti o dei relativi componenti di test nidificati (hostTests e deviceTests). Questo è il posto giusto per disattivare i test a livello di programmazione 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 variante finale (KotlinMultiplatformAndroidVariant). Qui puoi esaminare 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.variant.SingleArtifact.MERGED_MANIFEST)
            }
        }
    }
}