为 Android KMP 构建自定义 Gradle 插件

本指南介绍了如何通过自定义 Gradle 插件检测 Kotlin Multiplatform (KMP) 项目中的 Android 目标、与之交互以及对其进行配置。在为 build 编写惯例插件或为 KMP 项目发布 Gradle 插件时,请使用这些 API。

检查项目是否使用了 Kotlin Multiplatform 插件

为避免出错并确保插件仅在 KMP 存在时运行,您必须检查项目是否使用了 KMP 插件。最佳实践是使用 plugins.withId() 对 KMP 插件的应用做出响应,而不是立即检查该插件。这种被动方法可防止插件因用户 build 脚本中插件的应用顺序而变得脆弱。

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