Tworzenie niestandardowych wtyczek Gradle na potrzeby Androida KMP

Z tego przewodnika dowiesz się, jak wykrywać, konfigurować i używać środowiska docelowego Androida w projekcie Kotlin Multiplatform (KMP) z niestandardowej wtyczki do obsługi Gradle. Używaj tych interfejsów API podczas pisania wtyczek konwencji do kompilacji lub publikowania wtyczek Gradle do projektów KMP.

Sprawdzanie, czy projekt używa wtyczki Kotlin Multiplatform

Aby uniknąć błędów i mieć pewność, że wtyczka działa tylko wtedy, gdy KMP jest obecny, musisz sprawdzić, czy projekt używa wtyczki KMP. Sprawdzoną metodą jest użycie plugins.withId() do reagowania na zastosowanie wtyczki KMP, zamiast natychmiastowego sprawdzania, czy została zastosowana. Takie reaktywne podejście zapobiega niestabilności wtyczki w zależności od kolejności, w jakiej wtyczki są stosowane w skryptach kompilacji użytkownika.

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

Uzyskiwanie dostępu do modelu

Punktem wejścia dla wszystkich konfiguracji Kotlin Multiplatform jest rozszerzenie 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)
        }
    }
}

Reagowanie na cele Kotlin Multiplatform

Użyj kontenera targets, aby reaktywnie konfigurować wtyczkę dla każdego celu dodanego przez użytkownika.

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

Zastosuj logikę związaną z celami

Jeśli wtyczka musi stosować logikę tylko do określonych typów platform, typowym podejściem jest sprawdzenie właściwości platformType. Jest to typ wyliczeniowy, który ogólnie kategoryzuje cel.

Możesz na przykład użyć tej opcji, jeśli wtyczka musi tylko rozróżniać ogólne środowiska (np. działać tylko na platformach podobnych do 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) */ }
                }
            }
        }
    }
}

Szczegóły dotyczące Androida

Wszystkie cele Androida mają wskaźnik platformType.androidJvm, ale KMP ma 2 różne punkty integracji w zależności od używanej wtyczki Androida do obsługi Gradle: KotlinAndroidTarget w przypadku projektów korzystających z com.android.library lub com.android.application oraz KotlinMultiplatformAndroidLibraryTarget w przypadku projektów korzystających z com.android.kotlin.multiplatform.library.

Interfejs API KotlinMultiplatformAndroidLibraryTarget został dodany w AGP 8.8.0, więc jeśli użytkownicy Twojej wtyczki korzystają z niższej wersji AGP, sprawdzenie target is KotlinMultiplatformAndroidLibraryTarget może spowodować błąd ClassNotFoundException. Aby było to bezpieczne, przed sprawdzeniem typu celu sprawdź AndroidPluginVersion.getCurrent(). AndroidPluginVersion.getCurrent() wymaga AGP w wersji 8.3.1 lub nowszej.

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

Uzyskiwanie dostępu do rozszerzenia Android KMP i jego właściwości

Wtyczka wchodzi w interakcję głównie z rozszerzeniem Kotlin udostępnianym przez wtyczkę Kotlin Multiplatform i rozszerzeniem Androida udostępnianym przez AGP dla celu KMP na Androida. Blok android {} w rozszerzeniu Kotlin w projekcie KMP jest reprezentowany przez interfejs KotlinMultiplatformAndroidLibraryTarget, który rozszerza też KotlinMultiplatformAndroidLibraryExtension. Oznacza to, że za pomocą tego jednego obiektu możesz uzyskać dostęp zarówno do właściwości DSL związanych z platformą docelową, jak i z Androidem.

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

W przeciwieństwie do innych wtyczek Androida (takich jak com.android.library lub com.android.application) wtyczka KMP Android nie rejestruje głównego rozszerzenia DSL na poziomie projektu. Znajduje się w hierarchii docelowej KMP, aby mieć pewność, że dotyczy tylko konkretnego celu Androida zdefiniowanego w konfiguracji wieloplatformowej.

Obsługa kompilacji i zbiorów źródeł

Często wtyczki muszą działać na bardziej szczegółowym poziomie niż tylko na poziomie celu, a konkretnie na poziomie kompilacji. KotlinMultiplatformAndroidLibraryTarget zawiera instancje KotlinMultiplatformAndroidCompilation (np. main, hostTest, deviceTest). Każda kompilacja jest powiązana ze zbiorami źródeł Kotlina. Wtyczki mogą wchodzić z nimi w interakcje, aby dodawać źródła, zależności lub konfigurować zadania kompilacji.

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

                    }
                }
            }
        }
    }
}

Konfigurowanie kompilacji testowych we wtyczkach konwencji

Podczas konfigurowania wartości domyślnych kompilacji testowych (takich jak targetSdk w przypadku testów z instrumentacją) we wtyczce konwencji unikaj używania metod aktywujących, takich jak withDeviceTest { } lub withHostTest { }. Wywołanie tych metod powoduje utworzenie odpowiednich wariantów testowych Androida i kompilacji dla każdego modułu, który stosuje wtyczkę konwencji, co może nie być odpowiednie. Nie można też wywołać tych metod po raz drugi w określonym module, aby doprecyzować ustawienia, ponieważ spowoduje to błąd informujący, że kompilacja została już utworzona.

Zamiast tego użyj reaktywnego bloku configureEach w kontenerze kompilacji. Dzięki temu możesz podać domyślne konfiguracje, które mają zastosowanie tylko wtedy, gdy moduł wyraźnie włącza kompilację testową:

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

Ten wzorzec sprawia, że wtyczka konwencji pozostaje leniwa i umożliwia poszczególnym modułom wywoływanie funkcji withDeviceTest { } w celu włączenia i dalszego dostosowywania testów bez konfliktów z ustawieniami domyślnymi.

Interakcja z interfejsem Variant API

W przypadku zadań, które wymagają konfiguracji na późnym etapie, dostępu do artefaktów (takich jak pliki manifestu lub kod bajtowy) albo możliwości włączania lub wyłączania określonych komponentów, musisz użyć interfejsu Android Variant API. W projektach KMP rozszerzenie jest typu KotlinMultiplatformAndroidComponentsExtension.

Rozszerzenie jest rejestrowane na poziomie projektu, gdy stosowana jest wtyczka Androida KMP.

Użyj beforeVariants, aby kontrolować tworzenie wariantów lub ich zagnieżdżonych komponentów testowych (hostTests i deviceTests). To właściwe miejsce do automatycznego wyłączania testów lub zmieniania wartości właściwości 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 }
            }
        }
    }
}

Użyj onVariants, aby uzyskać dostęp do obiektu ostatecznego wariantu (KotlinMultiplatformAndroidVariant). Tutaj możesz sprawdzać rozwiązane właściwości lub rejestrować przekształcenia artefaktów, takich jak manifest łączony czy klasy biblioteki.

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