Criar plug-ins personalizados do Gradle para KMP do Android

Este documento fornece um guia para autores de plug-ins sobre como detectar, interagir e configurar corretamente a configuração do Kotlin Multiplatform (KMP), com foco específico na integração com as metas do Android em um projeto KMP. À medida que o KMP continua evoluindo, entender os hooks e as APIs adequados, como KotlinMultiplatformExtension, tipos KotlinTarget e as interfaces de integração específicas do Android, é essencial para criar ferramentas robustas e preparadas para o futuro que funcionam perfeitamente em todas as plataformas definidas em um projeto multiplataforma.

Verificar se um projeto usa o plug-in Kotlin Multiplatform

Para evitar erros e garantir que o plug-in só seja executado quando a KMP estiver presente, verifique se o projeto usa o plug-in KMP. A prática recomendada é usar plugins.withId() para reagir à aplicação do plug-in KMP, em vez de verificar imediatamente. Essa abordagem reativa evita que seu plug-in seja frágil à 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 de forma reativa seu plug-in 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. É uma enumeração que categoriza o destino de forma geral.

Por exemplo, use isso se o plug-in precisar apenas diferenciar de maneira geral (por exemplo, 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 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) */ }
                }
            }
        }
    }
}

Detalhes específicos do Android

Embora todos os destinos do Android tenham o indicador platformType.androidJvm, a 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 mais antiga do AGP, a verificação de target is KotlinMultiplatformAndroidLibraryTarget poderá resultar em um ClassNotFoundException. Para garantir a segurança, verifique AndroidPluginVersion.getCurrent() antes de verificar o tipo de destino. O AndroidPluginVersion.getCurrent() requer o AGP 7.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.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
                }
            }
        }
    }
}

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

Seu plug-in vai interagir principalmente com a extensão Kotlin fornecida pelo plug-in Kotlin Multiplatform e a extensão Android fornecida pelo AGP para o destino 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 você pode acessar propriedades de DSL específicas do destino e do Android usando 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 KMP não registra a extensão principal DSL no nível do projeto. Ele fica na hierarquia de destino do KMP para garantir que só se aplique 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 trabalhar em um nível mais granular do que apenas o destino. Especificamente, eles precisam trabalhar 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 fontes, 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 instrumentados) 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 e compilações correspondentes do Android para cada módulo que aplica o plug-in de convenção, o que pode não ser adequado. Além disso, esses métodos não podem ser chamados uma segunda vez em um módulo específico para refinar as configurações, porque isso vai gerar um erro informando que a compilação já foi criada.

Em vez disso, recomendamos usar um bloco configureEach reativo no contêiner de compilações. Isso permite fornecer 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 {
                        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 conflitar com os padrões.

Interagir com a API Variant

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

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

Use beforeVariants para controlar a criação de variantes ou os componentes de teste aninhados (hostTests e deviceTests). Este é o lugar certo para desativar testes ou mudar os valores das propriedades da DSL de maneira programática.

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 mesclado ou 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.variant.SingleArtifact.MERGED_MANIFEST)
            }
        }
    }
}