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

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

ตรวจสอบว่าโปรเจ็กต์ใช้ปลั๊กอิน 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 นี่คือการแจงนับที่จัดหมวดหมู่เป้าหมายในวงกว้าง

เช่น ใช้ในกรณีที่ปลั๊กอินของคุณต้องการแยกความแตกต่างในวงกว้างเท่านั้น (เช่น เรียกใช้เฉพาะในเป้าหมายที่คล้าย 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

เราได้เพิ่ม KotlinMultiplatformAndroidLibraryTarget API ใน 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
                }
            }
        }
    }
}

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

จัดการการรวบรวมและชุดแหล่งข้อมูล

บ่อยครั้งที่ปลั๊กอินต้องทำงานในระดับที่ละเอียดยิ่งกว่าแค่เป้าหมาย กล่าวคือ ต้องทำงานในระดับการคอมไพล์ 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 ->

                    }
                }
            }
        }
    }
}

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

เมื่อกำหนดค่าเริ่มต้นสำหรับการคอมไพล์การทดสอบ (เช่น targetSdk สำหรับ การทดสอบที่มีการตรวจสอบ) ��นปลั๊กอินของรูปแบบ คุณควรหลีกเลี่ยงการใช้เมธอดที่เปิดใช้ เช่น withDeviceTest { } หรือ withHostTest { } การเรียกใช้เมธอดเหล่านี้ จะทริกเกอร์การสร้างตัวแปรทดสอบ Android ที่เกี่ยวข้องและการ คอมไพล์สำหรับทุกโมดูลที่ใช้ปลั๊กอิน Convention ซึ่งอาจ ไม่เหมาะสม นอกจากนี้ คุณจะเรียกใช้เมธอดเหล่านี้เป็นครั้งที่ 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 {
                        targetSdk { version = release(34) }
                    }
                }
        }
    }
}

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