Создание пользовательских плагинов Gradle для Android KMP

В этом руководстве объясняется, как обнаруживать целевой объект Android, взаимодействовать с ним и настраивать его в проекте Kotlin Multiplatform (KMP) из пользовательского плагина Gradle. Используйте эти API при написании плагинов соглашений для сборки или публикации плагинов Gradle для проектов KMP.

Как проверить, используется ли в проекте плагин 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: KotlinAndroidTarget для проектов, использующих com.android.library или com.android.application, и KotlinMultiplatformAndroidLibraryTarget для проектов, использующих com.android.kotlin.multiplatform.library.

API KotlinMultiplatformAndroidLibraryTarget был добавлен в 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, предоставляемым плагином Kotlin Multiplatform, и расширением Android, предоставляемым AGP для целевой платформы KMP Android. Блок 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
            }
        }
    }
}

В отличие от других плагинов 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)
            }
        }
    }
}