Créer des plug-ins Gradle personnalisés pour Android KMP

Ce guide explique comment détecter, configurer et interagir avec la cible Android dans un projet Kotlin Multiplatform (KMP) à partir d'un plug-in Gradle personnalisé. Utilisez ces API lorsque vous écrivez des plug-ins de convention pour votre build ou que vous publiez des plug-ins Gradle pour des projets KMP.

Vérifier si un projet utilise le plug-in Kotlin Multiplatform

Pour éviter les erreurs et vous assurer que votre plug-in ne s'exécute que lorsque KMP est présent, vous devez vérifier si le projet utilise le plug-in KMP. Il est recommandé d'utiliser plugins.withId() pour réagir à l'application du plug-in KMP, plutôt que de le vérifier immédiatement. Cette approche réactive empêche votre plug-in d'être sensible à l'ordre dans lequel les plug-ins sont appliqués dans les scripts de compilation de l'utilisateur.

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

Accéder au modèle

Le point d'entrée de toutes les configurations Kotlin Multiplatform est l'extension 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)
        }
    }
}

Réagir aux cibles Kotlin Multiplatform

Utilisez le conteneur targets pour configurer de manière réactive votre plug-in pour chaque cible ajoutée par l'utilisateur.

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

Appliquer une logique spécifique à la cible

Si votre plug-in ne doit appliquer une logique qu'à certains types de plates-formes, une approche courante consiste à vérifier la propriété platformType. Il s'agit d'une énumération qui classe globalement la cible.

Par exemple, utilisez cette option si votre plug-in n'a besoin que d'une différenciation générale (par exemple, s'il ne s'exécute que sur des cibles de type 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) */ }
                }
            }
        }
    }
}

Informations spécifiques à Android

Bien que toutes les cibles Android disposent de l'indicateur platformType.androidJvm, KMP comporte deux points d'intégration distincts en fonction du plug-in Android Gradle utilisé : KotlinAndroidTarget pour les projets utilisant com.android.library ou com.android.application, et KotlinMultiplatformAndroidLibraryTarget pour les projets utilisant com.android.kotlin.multiplatform.library.

L'API KotlinMultiplatformAndroidLibraryTarget a été ajoutée dans AGP 8.8.0. Par conséquent, si les utilisateurs de votre plug-in exécutent une version antérieure d'AGP, la vérification de target is KotlinMultiplatformAndroidLibraryTarget peut entraîner une ClassNotFoundException. Pour que cette opération soit sécurisée, vérifiez AndroidPluginVersion.getCurrent() avant de vérifier le type de cible. AndroidPluginVersion.getCurrent() nécessite AGP 8.3.1 ou une version ultérieure.

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

Accéder à l'extension KMP Android et à ses propriétés

Votre plug-in interagit principalement avec l'extension Kotlin fournie par le plug-in Kotlin Multiplatform et l'extension Android fournie par AGP pour la cible Android KMP. Le bloc android {} de l'extension Kotlin dans un projet KMP est représenté par l'interface KotlinMultiplatformAndroidLibraryTarget, qui étend également KotlinMultiplatformAndroidLibraryExtension. Cela signifie que vous pouvez accéder aux propriétés DSL spécifiques à la cible et à Android via cet objet unique.

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

Contrairement aux autres plug-ins Android (comme com.android.library ou com.android.application), le plug-in KMP Android n'enregistre pas son extension DSL principale au niveau du projet. Il se trouve dans la hiérarchie des cibles KMP pour s'assurer qu'il ne s'applique qu'à la cible Android spécifique définie dans votre configuration multiplate-forme.

Gérer les compilations et les ensembles de sources

Souvent, les plug-ins doivent fonctionner à un niveau plus précis que la cible, en particulier au niveau de la compilation. KotlinMultiplatformAndroidLibraryTarget contient des instances KotlinMultiplatformAndroidCompilation (par exemple, main, hostTest, deviceTest). Chaque compilation est associée à des ensembles de sources Kotlin. Les plug-ins peuvent interagir avec ces éléments pour ajouter des sources, des dépendances ou configurer des tâches de compilation.

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

                    }
                }
            }
        }
    }
}

Configurer des compilations de test dans des plug-ins de convention

Lorsque vous configurez des valeurs par défaut pour les compilations de test (comme targetSdk pour les tests d'instrumentation) dans un plug-in de convention, évitez d'utiliser des méthodes d'activation telles que withDeviceTest { } ou withHostTest { }. L'appel de ces méthodes déclenche de manière hâtive la création des variantes de test Android correspondantes et des compilations pour chaque module qui applique le plug-in de convention, ce qui peut ne pas être approprié. Ces méthodes ne peuvent pas non plus être appelées une deuxième fois dans un module spécifique pour affiner les paramètres, car cela génère une erreur indiquant que la compilation a déjà été créée.

Utilisez plutôt un bloc configureEach réactif sur le conteneur de compilations. Cela vous permet de fournir des configurations par défaut qui ne s'appliquent que si et quand un module active explicitement la compilation de 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) }
                    }
                }
        }
    }
}

Ce modèle garantit que votre plug-in de convention reste différé et permet aux modules individuels d'appeler withDeviceTest { } pour activer et personnaliser davantage leurs tests sans entrer en conflit avec les valeurs par défaut.

Interagir avec l'API Variant

Pour les tâches qui nécessitent une configuration tardive, l'accès aux artefacts (comme les fichiers manifestes ou le bytecode) ou la possibilité d'activer ou de désactiver des composants spécifiques, vous devez utiliser l'API Android Variant. Dans les projets KMP, l'extension est de type KotlinMultiplatformAndroidComponentsExtension.

L'extension est enregistrée au niveau du projet lorsque le plug-in KMP Android est appliqué.

Utilisez beforeVariants pour contrôler la création de variantes ou de leurs composants de test imbriqués (hostTests et deviceTests). C'est l'endroit idéal pour désactiver par programmation des tests ou modifier les valeurs des propriétés 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 }
            }
        }
    }
}

Utilisez onVariants pour accéder à l'objet de variante final (KotlinMultiplatformAndroidVariant). C'est là que vous pouvez inspecter les propriétés résolues ou enregistrer des transformations sur des artefacts tels que le fichier manifeste fusionné ou les classes de bibliothèque.

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