為 Android KMP 建構自訂 Gradle 外掛程式

這份文件提供外掛程式作者指南,說明如何正確偵測、互動及設定 Kotlin Multiplatform (KMP),並特別著重於整合 KMP 專案中的 Android 目標。隨著 KMP 持續發展,瞭解適當的掛鉤和 API (例如 KotlinMultiplatformExtensionKotlinTarget 型別和 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 有兩個不同的整合點,取決於使用的 Android Gradle 外掛程式:使用 com.android.librarycom.android.application 的專案為 KotlinAndroidTarget,使用 com.android.kotlin.multiplatform.library 的專案為 KotlinMultiplatformAndroidLibraryTarget

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

存取 Android KMP 擴充功能及其屬性

外掛程式主要會與 Kotlin Multiplatform 外掛程式提供的 Kotlin 擴充功能,以及 AGP 為 KMP Android 目標提供的 Android 擴充功能互動。KMP 專案中 Kotlin 擴充功能內的 android {} 區塊由 KotlinMultiplatformAndroidLibraryTarget 介面表示,該介面也會擴充 KotlinMultiplatformAndroidLibraryExtension。也就是說,您可以透過這個單一物件,存取目標專屬和 Android 專屬的 DSL 屬性。

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

不同於其他 Android 外掛程式 (例如 com.android.librarycom.android.application),KMP Android 外掛程式不會在專案層級註冊主要 DSL 擴充功能。它位於 KMP 目標階層中,確保只適用於多平台設定中定義的特定 Android 目標。

處理彙整和來源集

通常外掛程式需要比目標更精細的層級運作,具體來說,就是需要在編譯層級運作。KotlinMultiplatformAndroidLibraryTarget 包含 KotlinMultiplatformAndroidCompilation 執行個體 (例如 mainhostTestdeviceTest)。每個編譯作業都與 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 互動

如要執行需要後期設定、存取構件 (例如資訊清單或位元碼),或啟用/停用特定元件的作業,請務必使用 Android Variant API。在 KMP 專案中,擴充功能屬於 KotlinMultiplatformAndroidComponentsExtension 類型。

套用 KMP Android 外掛程式時,系統會在專案層級註冊擴充功能。

使用 beforeVariants 控制變異版本或其巢狀測試元件 (hostTestsdeviceTests) 的建立作業。這是以程式停用測試或變更 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)。您可以在這裡檢查已解析的屬性,或在合併資訊清單或程式庫類別等構件上註冊轉換。

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