为 Android KMP 构建自定义 Gradle 插件

本文档为插件作者提供了一份指南,介绍了如何正确检测、交互和配置 Kotlin Multiplatform (KMP) 设置,重点是与 KMP 项目中的 Android 目标平台集成。无论您是构建惯例插件来标准化项目模块的配置,还是开发插件以供更广泛的社区使用,这些建议都适用。随着 KMP 的不断发展,了解适当的钩子和 API(例如 KotlinMultiplatformExtensionKotlinTarget 类型和特定于 Android 的集成接口)对于构建可在多平台项目中定义的所有平台上无缝运行的强大且面向未来的工具至关重要。

检查项目是否使用了 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 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 插件:KotlinAndroidTarget 适用于使用 com.android.librarycom.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
                }
            }
        }
    }
}

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