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

เอกสารนี้เป็นคู่มือสำหรับผู้เขียนปลั๊กอินเกี่ยวกับวิธีตรวจหา โต้ตอบ และกำหนดค่าการตั้งค่า Kotlin Multiplatform (KMP) อย่างถูกต้อง โดยเน้นที่การผสานรวมกับเป้าหมาย Android ภายในโปรเจ็กต์ KMP คำแนะนำเหล่านี้มีผลไม่ว่าคุณจะสร้างปลั๊กอินตามข้อกำหนดเพื่อกำหนดการกำหนดค่ามาตรฐานในโมดูลของโปรเจ็กต์ หรือพัฒนาปลั๊กอินเพื่อใช้ในชุมชนในวงกว้าง เนื่องจาก KMP มีการพัฒนาอย่างต่อเนื่อง การทำความเข้าใจฮุกและ API ที่เหมาะสม เช่น KotlinMultiplatformExtension, ประเภท KotlinTarget และอินเทอร์เฟซการผสานรวมเฉพาะของ Android จึงเป็นสิ่งสำคัญสำหรับการสร้างเครื่องมือที่แข็งแกร่งและพร้อมรับมือกับการเปลี่ยนแปลงในอนาคต ซึ่งทำงานได้อย่างราบรื่นในทุกแพลตฟอร์มที่กำหนดไว้ในโปรเจ็กต์ Multiplatform

ตรวจสอบว่าโปรเจ็กต์ใช้ปลั๊กอิน 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 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) */ }
                }
            }
        }
    }
}

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

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

API KotlinMultiplatformAndroidLibraryTarget ได้รับการเพิ่มใน AGP 8.8.0 ดังนั้นหาก ผู้ใช้ปลั๊กอินของคุณใช้ AGP เวอร์ชันต่ำกว่า การตรวจสอบ target is KotlinMultiplatformAndroidLibraryTarget อาจทำให้เกิด ClassNotFoundException หากต้องการทำให้ปลอดภัย ให้ตรวจสอบ AndroidPluginVersion.getCurrent() ก่อนตรวจสอบประเภทเป้าหมาย โปรดทราบว่า AndroidPluginVersion.getCurrent() ต้องใช้ AGP 7.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.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
                }
            }
        }
    }
}

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

ปลั๊กอินของคุณจะโต้ตอบกับส่วนขยาย Kotlin ที่ปลั๊กอิน Kotlin Multiplatform มีให้และส่วนขยาย Android ที่ AGP มีให้สำหรับเป้าหมาย KMP Android เป็นหลัก บล็อก 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 Plugin ไม่ได้ลงทะเบียนส่วนขยาย DSL หลักที่ระดับโปรเจ็กต์ ซึ่งแตกต่างจากปลั๊กอิน Android อื่นๆ (เช่น com.android.library หรือ com.android.application) ส่วนขยายนี้อยู่ในลำดับชั้นเป้าหมาย KMP เพื่อให้แน่ใจว่าส่วนขยายจะใช้ได้กับเป้าหมาย Android ที่เฉพาะเจาะจงซึ่งกำหนดไว้ในการตั้งค่า Multiplatform เท่านั้น

จัดการการคอมไพล์และชุดแหล่งที่มา

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

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 และการคอมไพล์ที่เกี่ยวข้องสำหรับทุกโมดูลที่ใช้ปลั๊กอินตามข้อกำหนด ซึ่งอาจไม่เหมาะสม นอกจากนี้ คุณยังเรียกวิธีการเหล่านี้ซ้ำในโมดูลที่เฉพาะเจาะจงเพื่อปรับแต่งการตั้งค่าไม่ได้ เนื่องจากระบบจะแสดงข้อผิดพลาดที่ระบุว่าได้สร้างการคอมไพล์แล้ว

เราขอแนะนำให้ใช้บล็อก 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 {
                        targetSdk { version = release(34) }
                    }
                }
        }
    }
}

รูปแบบนี้ช่วยให้ปลั๊กอินตามข้อกำหนดยังคงทำงานแบบเลื่อนออก และช่วยให้โมดูลแต่ละโมดูลเรียก 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.variant.SingleArtifact.MERGED_MANIFEST)
            }
        }
    }
}