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