Android KMP 用のカスタム Gradle プラグインをビルドする

このドキュメントでは、Kotlin マルチプラットフォーム(KMP)の設定を正しく検出し、操作し、構成する方法について、プラグイン作成者向けにガイドを提供します。特に、KMP プロジェクト内の Android ターゲットとの統合に焦点を当てています。KMP は進化を続けているため、KotlinMultiplatformExtensionKotlinTarget 型、Android 固有の統合インターフェースなどの適切なフックと API を理解することは、マルチプラットフォーム プロジェクトで定義されたすべてのプラットフォームでシームレスに動作する堅牢で将来性のあるツールを構築するために不可欠です。

プロジェクトで Kotlin Multiplatform プラグインが使用されているかどうかを確認する

エラーを回避し、KMP が存在する場合にのみプラグインが実行されるようにするには、プロジェクトで KMP プラグインが使用されているかどうかを確認する必要があります。KMP プラグインが適用されたことをすぐに確認するのではなく、plugins.withId() を使用して対応することをおすすめします。このリアクティブなアプローチにより、ユーザーのビルドスクリプトでプラグインが適用される順序にプラグインが左右されることを防ぎます。

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 プラグインに応じて 2 つの異なる統合ポイントがあります。com.android.library または com.android.application を使用するプロジェクトの場合は KotlinAndroidTargetcom.android.kotlin.multiplatform.library を使用するプロジェクトの場合は KotlinMultiplatformAndroidLibraryTarget です。

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 拡張機能と、KMP Android ターゲット用の AGP が提供する Android 拡張機能とやり取りします。KMP プロジェクトの Kotlin 拡張機能内の android {} ブロックは KotlinMultiplatformAndroidLibraryTarget インターフェースで表されます。このインターフェースは KotlinMultiplatformAndroidLibraryExtension も拡張します。つまり、この単一のオブジェクトを介して、ターゲット固有の DSL プロパティと 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 テスト バリアントとコンパイルの作成がトリガーされます。これは適切でない可能性があります。また、これらのメソッドを特定のモジュールで 2 回呼び出して設定を調整することはできません。呼び出すと、コンパイルがすでに作成されていることを示すエラーがスローされます。

代わりに、コンパイル コンテナでリアクティブな 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)
            }
        }
    }
}