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

本指南說明如何透過自訂 Gradle 外掛程式,偵測、設定 Kotlin Multiplatform (KMP) 專案中的 Android 目標,並與其互動。為建構作業編寫慣例外掛程式,或為 KMP 專案發布 Gradle 外掛程式時,請使用這些 API。

檢查專案是否使用 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 */ }
                    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.library 或 com.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 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
                }
            }
        }
    }
}

存取 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.library 或 com.android.application) 不同,KMP Android 外掛程式不會在專案層級註冊主要 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 ->

                    }
                }
            }
        }
    }
}

在慣例外掛程式中設定測試編譯

在慣例外掛程式中設定測試編譯的預設值 (例如檢測設備測試的 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 {
                        it.targetSdk { version = release(34) }
                    }
                }
        }
    }
}

這種模式可確保慣例外掛程式維持延遲狀態,並允許個別模組呼叫 withDeviceTest { },以便啟用及進一步自訂測試,而不會與預設值衝突。

與 Variant API 互動

如果工作需要後期設定、構件存取權 (例如資訊清單或位元碼),或啟用/停用特定元件的功能,則必須使用 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)。您可以在這裡檢查已解析的屬性,或在合併的資訊清單或程式庫類別等構件上註冊轉換。

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