Criar plug-ins personalizados do Gradle para KMP do Android

Este guia explica como detectar, interagir e configurar o destino do Android em um projeto Kotlin Multiplatform (KMP) usando um plug-in personalizado do Gradle. Use essas APIs ao escrever plug-ins de convenção para seu build ou publicar plug-ins do Gradle para projetos KMP.

Verificar se um projeto usa o plug-in do Kotlin Multiplatform

Para evitar erros e garantir que o plug-in seja executado apenas quando o KMP estiver presente, verifique se o projeto usa o plug-in do KMP. A prática recomendada é usar plugins.withId() para reagir à aplicação do plug-in do KMP, em vez de verificar imediatamente. Essa abordagem reativa impede que o plug-in seja frágil em relação à ordem em que os plug-ins são aplicados nos scripts de build do usuário.

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

Acessar o modelo

O ponto de entrada para todas as configurações do Kotlin Multiplatform é a extensão 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)
        }
    }
}

Reagir a destinos do Kotlin Multiplatform

Use o contêiner targets para configurar o plug-in de forma reativa para cada destino adicionado pelo usuário.

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

Aplicar lógica específica do destino

Se o plug-in precisar aplicar lógica apenas a determinados tipos de plataformas, uma abordagem comum é verificar a propriedade platformType. Este é um tipo enumerado que categoriza amplamente o destino.

Por exemplo, use isso se o plug-in só precisar fazer uma diferenciação ampla, como executar apenas em destinos semelhantes à 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) */ }
                }
            }
        }
    }
}

Detalhes específicos do Android

Embora todos os destinos do Android tenham o indicador platformType.androidJvm, o KMP tem dois pontos de integração distintos, dependendo do Plug-in do Android para Gradle usado: KotlinAndroidTarget para projetos que usam com.android.library ou com.android.application e KotlinMultiplatformAndroidLibraryTarget para projetos que usam com.android.kotlin.multiplatform.library.

A API KotlinMultiplatformAndroidLibraryTarget foi adicionada no AGP 8.8.0. Portanto, se os consumidores do seu plug-in estiverem usando uma versão anterior do AGP, a verificação de target is KotlinMultiplatformAndroidLibraryTarget pode resultar em um ClassNotFoundException. Para que isso seja seguro, verifique AndroidPluginVersion.getCurrent() antes de verificar o tipo de destino. O AndroidPluginVersion.getCurrent() requer o AGP 8.3.1 ou versões mais recentes.

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

Acessar a extensão do KMP para Android e as propriedades dela

Seu plug-in interage principalmente com a extensão do Kotlin fornecida pelo plug-in do Kotlin Multiplatform e com a extensão do Android fornecida pelo AGP para o destino do Android do KMP. O bloco android {} na extensão Kotlin em um projeto KMP é representado pela interface KotlinMultiplatformAndroidLibraryTarget, que também estende KotlinMultiplatformAndroidLibraryExtension. Isso significa que é possível acessar as propriedades de DSL específicas do destino e do Android por esse único objeto.

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

Ao contrário de outros plug-ins do Android (como com.android.library ou com.android.application), o plug-in do Android para KMP não registra a extensão principal da DSL no nível do projeto. Ele fica na hierarquia de destino do KMP para garantir que seja aplicado apenas ao destino específico do Android definido na sua configuração multiplataforma.

Processar compilações e conjuntos de origem

Muitas vezes, os plug-ins precisam funcionar em um nível mais granular do que apenas o destino, especificamente no nível de compilação. O KotlinMultiplatformAndroidLibraryTarget contém instâncias de KotlinMultiplatformAndroidCompilation (por exemplo, main, hostTest, deviceTest). Cada compilação está associada a conjuntos de origem do Kotlin. Os plug-ins podem interagir com eles para adicionar origens, dependências ou configurar tarefas de compilação.

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

                    }
                }
            }
        }
    }
}

Configurar compilações de teste em plug-ins de convenção

Ao configurar valores padrão para compilações de teste (como targetSdk para testes de instrumentação) em um plug-in de convenção, evite usar métodos de ativação, como withDeviceTest { } ou withHostTest { }. Chamar esses métodos aciona a criação das variantes de teste do Android correspondentes e as compilações para cada módulo que aplica o plug-in de convenção, o que pode não ser adequado. Esses métodos também não podem ser chamados uma segunda vez em um módulo específico para refinar as configurações, porque isso gera um erro informando que a compilação já foi criada.

Em vez disso, use um bloco configureEach reativo no contêiner de compilações. Isso permite que você forneça configurações padrão que só se aplicam se e quando um módulo ativa explicitamente a compilação de teste:

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

Esse padrão garante que o plug-in de convenção permaneça lento e permite que módulos individuais chamem withDeviceTest { } para ativar e personalizar ainda mais os testes sem entrar em conflito com os padrões.

Interagir com a API Variant

Para tarefas que exigem configuração de estágio final, acesso a artefatos (como manifestos ou bytecode) ou a capacidade de ativar ou desativar componentes específicos, use a API Android Variant. Em projetos KMP, a extensão é do tipo KotlinMultiplatformAndroidComponentsExtension.

A extensão é registrada no nível do projeto quando o plug-in do Android para KMP é aplicado.

Use beforeVariants para controlar a criação de variantes ou os componentes de teste aninhados (hostTests e deviceTests). Esse é o lugar correto para desativar testes de maneira programática ou mudar os valores das propriedades da 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 }
            }
        }
    }
}

Use onVariants para acessar o objeto de variante final (KotlinMultiplatformAndroidVariant). É aqui que você pode inspecionar propriedades resolvidas ou registrar transformações em artefatos, como o manifesto integrado ou as classes de biblioteca.

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