สร้างปลั๊กอิน Gradle ที่กำหนดเองสำหรับ KMP ของ Android

คู่มือนี้อธิบายวิธีตรวจจับ โต้ตอบ และกำหนดค่าเป้าหมาย Android ในโปรเจ็กต์ Kotlin Multiplatform (KMP) จากปลั๊กอิน Gradle ที่กำหนดเอง ใช้ API เหล่านี้เมื่อเขียนปลั๊กอินแบบแผนสำหรับบิลด์หรือเผยแพร่ปลั๊กอิน Gradle สำหรับโปรเจ็กต์ KMP

ตรวจสอบว่าโปรเจ็กต์ใช้ปลั๊กอิน Kotlin Multiplatform หรือไม่

เพื่อหลีกเลี่ยงข้อผิดพลาดและตรวจสอบว่าปลั๊กอินจะทำงานเมื่อมี KMP เท่านั้น คุณต้องตรวจสอบว่าโปรเจ็กต์ใช้ปลั๊กอิน KMP หรือไม่ แนวทางปฏิบัติแนะนำคือใช้ plugins.withId() เพื่อตอบสนองต่อการใช้ปลั๊กอิน KMP แทนที่จะตรวจสอบทันที แนวทางเชิงรับนี้จะช่วยป้องกันไม่ให้ปลั๊กอินของคุณเปราะบางต่อลำดับการใช้ปลั๊กอินในสคริปต์บิลด์ของผู้ใช้

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

เข้าถึงโมเดล

จุดแรกเข้าสำหรับการกำหนดค่า Kotlin Multiplatform ทั้งหมดคือส่วนขยาย 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)
        }
    }
}

ตอบสนองต่อเป้าหมาย Kotlin Multiplatform

ใช้คอนเทนเนอร์ targets เพื่อกำหนดค่าปลั๊กอินแบบเชิงรับสำหรับแต่ละเป้าหมายที่ผู้ใช้เพิ่ม

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

ใช้ตรรกะเฉพาะเป้าหมาย

หากปลั๊กอินของคุณต้องใช้ตรรกะกับแพลตฟอร์มบางประเภทเท่านั้น วิธีการทั่วไปคือการตรวจสอบพร็อพเพอร์ตี้ platformType นี่คือ enum ที่จัดหมวดหมู่เป้าหมายอย่างกว้างๆ

ตัวอย่างเช่น ใช้ตัวเลือกนี้หากปลั๊กอินของคุณต้องการแยกความแตกต่างในวงกว้างเท่านั้น (เช่น ทำงานบนเป้าหมายที่คล้ายกับ 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) */ }
                }
            }
        }
    }
}

รายละเอียดเฉพาะของ Android

แม้ว่าเป้าหมาย Android ทั้งหมดจะมีตัวบ่งชี้ platformType.androidJvm แต่ KMP มีจุดผสานรวมที่แตกต่างกัน 2 จุด โดยขึ้นอยู่กับปลั๊กอิน Android Gradle ที่ใช้ ได้แก่ KotlinAndroidTarget สำหรับโปรเจ็กต์ที่ใช้ com.android.library หรือ com.android.application และ KotlinMultiplatformAndroidLibraryTarget สำหรับโปรเจ็กต์ที่ใช้ com.android.kotlin.multiplatform.library

เพิ่ม KotlinMultiplatformAndroidLibraryTarget API ใน AGP 8.8.0 แล้ว ดังนั้นหากผู้ใช้ปลั๊กอินของคุณใช้ AGP เวอร์ชันต่ำกว่า การตรวจสอบ target is KotlinMultiplatformAndroidLibraryTarget อาจส่งผลให้เกิด ClassNotFoundException หากต้องการให้การดำเนินการนี้ปลอดภัย ให้ตรวจสอบ AndroidPluginVersion.getCurrent() ก่อนตรวจสอบประเภทเป้าหมาย AndroidPluginVersion.getCurrent() ต้องใช้ AGP 8.3.1 ขึ้นไป

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

เข้าถึงส่วนขยาย KMP ของ Android และพร็อพเพอร์ตี้ของส่วนขยาย

ปลั๊กอินของคุณจะโต้ตอบกับส่วนขยาย Kotlin ที่ปลั๊กอิน Kotlin Multiplatform มีให้เป็นหลัก และส่วนขยาย Android ที่ AGP มีให้สำหรับเป้าหมาย Android ของ KMP บล็อก android {} ภายในส่วนขยาย Kotlin ในโปรเจ็กต์ KMP จะแสดงด้วยอินเทอร์เฟซ KotlinMultiplatformAndroidLibraryTarget ซึ่งขยาย KotlinMultiplatformAndroidLibraryExtension ด้วย ซึ่งหมายความว่าคุณสามารถเข้าถึงทั้งพร็อพเพอร์ตี้ DSL ที่เจาะจงเป้าหมายและเจาะจง Android ผ่านออบเจ็กต์เดียวนี้ได้

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

ปลั๊กอิน KMP Android ไม่ได้ลงทะเบียนส่วนขยาย DSL หลักที่ระดับโปรเจ็กต์ ซึ่งแตกต่างจากปลั๊กอิน Android อื่นๆ (เช่น com.android.library หรือ com.android.application) โดยจะอยู่ในลำดับชั้นเป้าหมาย KMP เพื่อให้แน่ใจว่าจะมีผลกับเป้าหมาย Android ที่เฉพาะเจาะจงซึ่งกำหนดไว้ในการตั้งค่าหลายแพลตฟอร์มเท่านั้น

จัดการการคอมไพล์และชุดซอร์ส

บ่อยครั้งที่ปลั๊กอินต้องทำงานในระดับที่ละเอียดยิ่งขึ้นกว่าแค่เป้าหมาย โดยเฉพาะในระดับการคอมไพล์ KotlinMultiplatformAndroidLibraryTarget มีอินสแตนซ์ KotlinMultiplatformAndroidCompilation (เช่น main, hostTest, deviceTest) การคอมไพล์แต่ละรายการจะเชื่อมโยงกับชุดซอร์ส Kotlin ปลั๊กอินสามารถโต้ตอบกับสิ่งเหล่านี้เพื่อเพิ่มแหล่งที่มา ทรัพยากร Dependency หรือกำหนดค่างานการคอมไพล์

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

                    }
                }
            }
        }
    }
}

กำหนดค่าการคอมไพล์การทดสอบในปลั๊กอินแบบแผน

เมื่อกำหนดค่าเริ่มต้นสำหรับการคอมไพล์การทดสอบ (เช่น targetSdk สำหรับการทดสอบการวัดคุม) ในปลั๊กอินแบบแผน ให้หลีกเลี่ยงการใช้เมธอดที่เปิดใช้ เช่น withDeviceTest { } หรือ withHostTest { } การเรียกใช้เมธอดเหล่านี้อย่างกระตือรือร้นจะทริกเกอร์การสร้างตัวแปรการทดสอบ Android ที่เกี่ยวข้องและการคอมไพล์สำหรับทุกโมดูลที่ใช้ปลั๊กอินแบบแผน ซึ่งอาจไม่เหมาะสม นอกจากนี้ คุณยังเรียกใช้เมธอดเหล่านี้เป็นครั้งที่ 2 ในโมดูลที่เฉพาะเจาะจงเพื่อปรับแต่งการตั้งค่าไม่ได้ เนื่องจากจะทำให้เกิดข้อผิดพลาดที่ระบุว่ามีการสร้างการคอมไพล์แล้ว

แต่ให้ใช้บล็อก configureEach แบบรีแอ็กทีฟในคอนเทนเนอร์การคอมไพล์แทน ซึ่งจะช่วยให้คุณระบุการกำหนดค่าเริ่มต้นที่จะมีผลก็ต่อเมื่อโมดูลเปิดใช้การคอมไพล์การทดสอบอย่างชัดแจ้งเท่านั้น

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

รูปแบบนี้ช่วยให้มั่นใจได้ว่าปลั๊กอินแบบแผนจะยังคงเป็นแบบ Lazy และอนุญาตให้แต่ละโมดูลเรียกใช้ withDeviceTest { } เพื่อเปิดใช้และปรับแต่งการทดสอบเพิ่มเติมโดยไม่ขัดแย้งกับค่าเริ่มต้น

โต้ตอบกับ Variant API

สำหรับงานที่ต้องมีการกำหนดค่าในระยะหลัง การเข้าถึงอาร์ติแฟกต์ (เช่น ไฟล์ Manifest หรือไบต์โค้ด) หรือความสามารถในการเปิดหรือปิดคอมโพเนนต์ที่เฉพาะเจาะจง คุณต้องใช้ Android Variant API ในโปรเจ็กต์ KMP ส่วนขยายจะเป็นประเภท KotlinMultiplatformAndroidComponentsExtension

ส่วนขยายจะลงทะเบียนที่ระดับโปรเจ็กต์เมื่อใช้ปลั๊กอิน KMP Android

ใช้ beforeVariants เพื่อควบคุมการสร้างตัวแปรหรือคอมโพเนนต์การทดสอบที่ฝังอยู่ (hostTests และ deviceTests) ซึ่งเป็นตำแหน่งที่ถูกต้องในการปิดใช้การทดสอบหรือเปลี่ยนค่าของพร็อพเพอร์ตี้ 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 }
            }
        }
    }
}

ใช้ onVariants เพื่อเข้าถึงออบเจ็กต์ตัวแปรสุดท้าย (KotlinMultiplatformAndroidVariant) ซึ่งเป็นที่ที่คุณสามารถตรวจสอบพร็อพเพอร์ตี้ที่แก้ไขแล้วหรือลงทะเบียนการเปลี่ยนรูปแบบในอาร์ติแฟกต์ เช่น ไฟล์ Manifest ที่ผสานแล้วหรือคลาสไลบรารี

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