Membangun plugin Gradle kustom untuk KMP Android

Panduan ini menjelaskan cara mendeteksi, berinteraksi dengan, dan mengonfigurasi target Android dalam project Kotlin Multiplatform (KMP) dari plugin Gradle kustom. Gunakan API ini saat menulis plugin konvensi untuk build Anda atau memublikasikan plugin Gradle untuk project KMP.

Memeriksa apakah project menggunakan plugin Kotlin Multiplatform

Untuk menghindari error dan memastikan plugin Anda hanya berjalan saat KMP ada, Anda harus memeriksa apakah project menggunakan plugin KMP. Sebaiknya gunakan plugins.withId() untuk bereaksi terhadap plugin KMP yang diterapkan, daripada langsung memeriksanya. Pendekatan reaktif ini mencegah plugin Anda menjadi rentan terhadap urutan penerapan plugin dalam skrip build pengguna.

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

Mengakses model

Titik entri untuk semua konfigurasi Multiplatform Kotlin adalah ekstensi 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)
        }
    }
}

Bereaksi terhadap target Kotlin Multiplatform

Gunakan penampung targets untuk mengonfigurasi plugin secara reaktif untuk setiap target yang ditambahkan pengguna.

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

Menerapkan logika khusus target

Jika plugin Anda perlu menerapkan logika hanya ke jenis platform tertentu, pendekatan yang umum adalah memeriksa properti platformType. Ini adalah enum yang secara luas mengategorikan target.

Misalnya, gunakan ini jika plugin Anda hanya perlu membedakan secara luas (seperti hanya berjalan pada target seperti 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) */ }
                }
            }
        }
    }
}

Detail khusus Android

Meskipun semua target Android memiliki indikator platformType.androidJvm, KMP memiliki dua titik integrasi yang berbeda, bergantung pada plugin Android Gradle yang digunakan: KotlinAndroidTarget untuk project yang menggunakan com.android.library atau com.android.application, dan KotlinMultiplatformAndroidLibraryTarget untuk project yang menggunakan com.android.kotlin.multiplatform.library.

KotlinMultiplatformAndroidLibraryTarget API ditambahkan di AGP 8.8.0, jadi jika konsumen plugin Anda berjalan di versi AGP yang lebih rendah, memeriksa target is KotlinMultiplatformAndroidLibraryTarget mungkin akan menghasilkan ClassNotFoundException. Agar aman, periksa AndroidPluginVersion.getCurrent() sebelum memeriksa jenis penargetan. AndroidPluginVersion.getCurrent() memerlukan AGP 8.3.1 atau yang lebih tinggi.

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

Mengakses ekstensi KMP Android dan propertinya

Plugin Anda terutama berinteraksi dengan ekstensi Kotlin yang disediakan oleh plugin Multiplatform Kotlin dan ekstensi Android yang disediakan oleh AGP untuk target Android KMP. Blok android {} dalam ekstensi Kotlin di project KMP diwakili oleh antarmuka KotlinMultiplatformAndroidLibraryTarget, yang juga memperluas KotlinMultiplatformAndroidLibraryExtension. Artinya, Anda dapat mengakses properti DSL khusus target dan khusus Android melalui satu objek ini.

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

Tidak seperti plugin Android lainnya (seperti com.android.library atau com.android.application), plugin Android KMP tidak mendaftarkan ekstensi DSL utamanya di level project. Plugin ini berada dalam hierarki target KMP untuk memastikan plugin hanya berlaku untuk target Android tertentu yang ditentukan dalam penyiapan multiplatform Anda.

Menangani kompilasi dan set sumber

Sering kali, plugin perlu bekerja pada tingkat yang lebih terperinci daripada sekadar target, khususnya pada tingkat kompilasi. KotlinMultiplatformAndroidLibraryTarget berisi instance KotlinMultiplatformAndroidCompilation (misalnya, main, hostTest, deviceTest). Setiap kompilasi dikaitkan dengan set sumber Kotlin. Plugin dapat berinteraksi dengan plugin ini untuk menambahkan sumber, dependensi, atau mengonfigurasi tugas kompilasi.

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

                    }
                }
            }
        }
    }
}

Mengonfigurasi kompilasi pengujian di plugin konvensi

Saat mengonfigurasi nilai default untuk kompilasi pengujian (seperti targetSdk untuk uji instrumentasi) dalam plugin konvensi, hindari penggunaan metode pengaktif seperti withDeviceTest { } atau withHostTest { }. Memanggil metode ini secara langsung akan memicu pembuatan varian pengujian Android dan kompilasi yang sesuai untuk setiap modul yang menerapkan plugin konvensi, yang mungkin tidak sesuai. Metode ini juga tidak dapat dipanggil untuk kedua kalinya dalam modul tertentu untuk menyaring setelan, karena tindakan tersebut akan menampilkan error yang menyatakan bahwa kompilasi telah dibuat.

Sebagai gantinya, gunakan blok configureEach reaktif pada container kompilasi. Dengan begitu, Anda dapat memberikan konfigurasi default yang hanya berlaku jika dan saat modul secara eksplisit mengaktifkan kompilasi pengujian:

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

Pola ini memastikan bahwa plugin konvensi Anda tetap lambat dan memungkinkan modul individual memanggil withDeviceTest { } untuk mengaktifkan dan menyesuaikan pengujiannya lebih lanjut tanpa bertentangan dengan setelan default.

Berinteraksi dengan Variant API

Untuk tugas yang memerlukan konfigurasi tahap akhir, akses artefak (seperti manifes atau byte-code), atau kemampuan untuk mengaktifkan atau menonaktifkan komponen tertentu, Anda harus menggunakan Android Variant API. Dalam project KMP, ekstensi berjenis KotlinMultiplatformAndroidComponentsExtension.

Ekstensi ini didaftarkan di level project saat plugin Android KMP diterapkan.

Gunakan beforeVariants untuk mengontrol pembuatan varian atau komponen pengujian bertingkatnya (hostTests dan deviceTests). Ini adalah tempat yang tepat untuk menonaktifkan pengujian secara terprogram atau mengubah nilai properti 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 }
            }
        }
    }
}

Gunakan onVariants untuk mengakses objek varian akhir (KotlinMultiplatformAndroidVariant). Di sinilah Anda dapat memeriksa properti yang di-resolve atau mendaftarkan transformasi pada artefak seperti manifes gabungan atau class library.

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