Membangun plugin Gradle kustom untuk KMP Android

Dokumen ini memberikan panduan bagi penulis plugin tentang cara mendeteksi, berinteraksi dengan, dan mengonfigurasi penyiapan Kotlin Multiplatform (KMP) dengan benar, dengan fokus khusus pada integrasi dengan target Android dalam project KMP. Seiring dengan terus berkembangnya KMP, pemahaman tentang hook dan API yang tepat—seperti jenis KotlinMultiplatformExtension, KotlinTarget, dan antarmuka integrasi khusus Android—sangat penting untuk membangun alat yang andal dan siap digunakan di masa mendatang yang berfungsi dengan lancar di semua platform yang ditentukan dalam project multiplatform.

Memeriksa apakah project menggunakan plugin Multiplatform Kotlin

Untuk menghindari error dan memastikan plugin Anda hanya berjalan saat KMP ada, Anda harus memeriksa apakah project menggunakan plugin KMP. Praktik terbaiknya adalah menggunakan plugins.withId() untuk bereaksi terhadap penerapan plugin KMP, bukan memeriksanya secara langsung. 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 Multiplatform Kotlin

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 umum adalah memeriksa properti platformType. Ini adalah enum yang secara luas mengategorikan target.

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

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.

API KotlinMultiplatformAndroidLibraryTarget ditambahkan di AGP 8.8.0, jadi jika konsumen plugin Anda berjalan di versi AGP yang lebih rendah, memeriksa target is KotlinMultiplatformAndroidLibraryTarget dapat menyebabkan ClassNotFoundException. Agar aman, periksa AndroidPluginVersion.getCurrent() sebelum memeriksa jenis target. Perhatikan bahwa AndroidPluginVersion.getCurrent() memerlukan AGP 7.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.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
                }
            }
        }
    }
}

Mengakses ekstensi KMP Android dan propertinya

Plugin Anda terutama akan 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 tingkat project. File ini berada dalam hierarki target KMP untuk memastikan file ini hanya berlaku untuk target Android tertentu yang ditentukan dalam penyiapan multiplatform Anda.

Menangani kompilasi dan set sumber

Sering kali, plugin perlu berfungsi pada tingkat yang lebih terperinci daripada hanya target—khususnya, plugin perlu berfungsi pada tingkat kompilasi. KotlinMultiplatformAndroidLibraryTarget berisi instance KotlinMultiplatformAndroidCompilation (misalnya, main, hostTest, deviceTest). Setiap kompilasi dikaitkan dengan set sumber Kotlin. Plugin dapat berinteraksi dengan 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 pengujian instrumentasi) di plugin konvensi, Anda harus menghindari 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. Selain itu, metode ini tidak dapat dipanggil untuk kedua kalinya dalam modul tertentu untuk menyempurnakan setelan, karena tindakan tersebut akan memunculkan error yang menyatakan bahwa kompilasi telah dibuat.

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

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

Pola ini memastikan bahwa plugin konvensi Anda tetap dimuat secara lazy dan memungkinkan setiap modul memanggil withDeviceTest { } untuk mengaktifkan dan menyesuaikan lebih lanjut pengujiannya tanpa berkonflik dengan default.

Berinteraksi dengan Variant API

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

Ekstensi didaftarkan di tingkat project saat plugin Android KMP diterapkan.

Gunakan beforeVariants untuk mengontrol pembuatan varian atau komponen pengujian bertingkatnya (hostTests dan deviceTests). Di sinilah 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 sini Anda dapat memeriksa properti yang telah diselesaikan 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.variant.SingleArtifact.MERGED_MANIFEST)
            }
        }
    }
}