The Android Target: the Android-KMP Library Plugin and AGP 9

This content was generated with the assistance of AI and should be verified against the official Kotlin Multiplatform documentation before being relied on in production.

See the section bibliography for the reference material consulted while preparing these pages.

Every Kotlin Multiplatform (KMP) project that ships to Android needs an Android target, and for years that target was bolted on by applying com.android.library or com.android.application next to the Kotlin Multiplatform plugin. Android Gradle Plugin (AGP) 9 ends that arrangement. This page explains the replacement, the Android-KMP library plugin (com.android.kotlin.multiplatform.library), how it differs from the plugins you may know from the native Android build, why the Android app must live in its own androidApp module, and how to migrate an existing project.

The page targets the section baseline: Kotlin 2.4.20, AGP 9.4.1 and Gradle 9.8.0. Kotlin 2.4.20 is tested against Gradle 7.6.3 to 9.7.0 and AGP 8.5.2 to 9.3.1, so AGP 9.4.1 and Gradle 9.8.0 are slightly newer than the tested range; the minimum AGP for the new plugin itself is 8.10.0. The running example is TrailMate Everywhere, whose sharedLogic and sharedUI modules are Android-KMP libraries consumed by an androidApp module. The Kotlin language is not taught here (see the Kotlin Reference); neither are Android Gradle fundamentals, which live in Project Structure, Gradle and Build Variants. For the multiplatform-wide Gradle DSL see Gradle Configuration, the Multiplatform DSL and Compatibility.

Why the Android Target Changed

Android is the one target in a KMP project that is not built by Kotlin alone: it is built by AGP, a separate Gradle plugin with its own release train, its own DSL and its own variant model. Historically the two plugins were wired together through an adapter inside the Kotlin plugin.

  • The first adapter used a target named android. JetBrains later renamed it to androidTarget so that the name android could be reserved for a plugin written by the Android team for multiplatform use.

  • That plugin is com.android.kotlin.multiplatform.library, available from AGP 8.10 (the class KotlinMultiplatformAndroidLibraryTarget was added in AGP 8.8) and configured inside the kotlin {} block, as kotlin { android { } }.

  • With AGP 9.0 (released Q4 2025) the old combination became unsupported: the Kotlin Multiplatform Gradle plugin is no longer compatible with com.android.application or com.android.library. The APIs the old combination relied on are deprecated, need an explicit opt-in under AGP 9 and are expected to be removed in AGP 10.

The table summarizes how the three ways of getting an Android target compare.

Aspect androidTarget() + com.android.library (legacy) com.android.application + KMP plugin (legacy) com.android.kotlin.multiplatform.library

Role

Android target of a shared library module

Android app and shared code in one module

Android target of a shared library module

Works with AGP 9

Only with legacy variant API enabled

Not supported

Yes (designed for it)

DSL location

Top-level android {} plus kotlin { androidTarget() }

Top-level android {}

kotlin { android { } } only

Variants

Build types and product flavors

Build types and product flavors

One single variant

Android tests

Created by default

Created by default

Opt-in

Java compilation

On by default

On by default

Opt-in with withJava()

Android resources

On by default

On by default

Opt-in with androidResources { enable = true }

Source-set names

androidMain, androidUnitTest, androidInstrumentedTest

Same

androidMain, androidHostTest, androidDeviceTest

Future

Removal expected in AGP 10

Must become a separate androidApp module

The supported path

The plugin is only for library modules. Google states there is no direct replacement for configuring a Kotlin Multiplatform module with com.android.application; the documented migration is to extract the Android app into a separate Gradle module. That is the single biggest structural consequence of AGP 9 and the reason the recommended layout of this section has an androidApp module (see Project Structure, Targets, Source Sets and the Hierarchy Template).

Before and After AGP 9

The following diagram contrasts the module graph of TrailMate Everywhere in the legacy single-module layout (typical of projects generated before AGP 9, where one composeApp module held the shared code and the Android entry point) with the layout that AGP 9 requires.

flowchart LR subgraph before ["Before AGP 9 -- one module is app and library"] direction TB CA["composeApp
kotlin-multiplatform
+ com.android.application
shared code + MainActivity"] IOS1["iosApp (Xcode)"] DSK1["desktop entry point"] IOS1 -->|"framework"| CA DSK1 --- CA end subgraph after ["After AGP 9 -- one module per entry point"] direction TB AA["androidApp
com.android.application
MainActivity, manifest, signing"] SU["sharedUI
kotlin-multiplatform
+ com.android.kotlin.multiplatform.library"] SL["sharedLogic
kotlin-multiplatform
+ com.android.kotlin.multiplatform.library"] IOS2["iosApp (Xcode)"] DSK2["desktopApp"] AA --> SU SU --> SL IOS2 -->|"framework"| SU DSK2 --> SU end before ==> after

Three things changed. The Android application plugin moved out of the shared module and into androidApp; the shared modules switched from com.android.library (or the application plugin) to the Android-KMP library plugin; and every other platform entry point (iosApp, desktopApp, webApp) stayed an entry point rather than becoming a Gradle dependency of the shared code.

Applying the Android-KMP Library Plugin

Applying the plugin takes three edits: declare it in the version catalog, register it in the root build file with apply false so that all subprojects share one plugin classpath, and apply it in each shared module.

# gradle/libs.versions.toml
[versions]
agp = "9.4.1"
kotlin = "2.4.20"

[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
android-application = { id = "com.android.application", version.ref = "agp" }

In the root build.gradle.kts, add the same aliases with apply false.

The shared module then combines the Kotlin Multiplatform plugin with the new Android plugin and configures the Android target in an android {} block inside kotlin {}:

// sharedLogic/build.gradle.kts
plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.android.kotlin.multiplatform.library)
}

kotlin {
    android {
        namespace = "com.example.trailmate.shared.logic"
        compileSdk = 36
        minSdk = 24
    }

    iosArm64()
    iosSimulatorArm64()
    jvm("desktop")

    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.coroutines.core)
        }
        androidMain.dependencies {
            implementation(libs.androidx.core.ktx)
        }
    }
}

There is no top-level android {} extension any more. The Android DSL lives inside the target, which is why the same configuration reads kotlin { android { namespace = …​ } }. Some older JetBrains tutorials spell the block androidLibrary {}; the current Android Developers documentation uses android {}, and so does this section.

The three properties in the example are the minimum:

Property Meaning

namespace

The Kotlin/Java package used for the generated R class and the library’s manifest identity. It must be unique across modules, and it must differ from the app’s namespace.

compileSdk

The Android API level the library is compiled against (36 in the examples).

minSdk

The lowest API level the library supports. Compose Multiplatform requires at least API 21; TrailMate uses 24.

There is no targetSdk in a library: it is an application concept and belongs to androidApp. There is also no applicationId, versionCode or versionName, for the same reason.

Dependencies and Source Sets

Android-only dependencies go in the androidMain source set, using the same sourceSets {} block as every other target. The legacy trick of putting implementation(…​) in a top-level dependencies {} block to mean "Android only" no longer exists in shared modules; it was a frequent source of confusion because it mixed with commonMainImplementation(…​) in the same block.

The plugin creates androidMain and, only on demand, two test source sets:

Source set (directory) Compilation Purpose

androidMain (src/androidMain)

main

Android implementations, actual declarations, Android-only dependencies

androidHostTest (src/androidHostTest)

hostTest

Local JVM unit tests (formerly androidUnitTest)

androidDeviceTest (src/androidDeviceTest)

deviceTest

Instrumented tests on a device or emulator (formerly androidInstrumentedTest)

The Single-Variant Model

The most visible difference from com.android.library is that the Android-KMP plugin produces exactly one variant. There are no build types (debug, release), no product flavors and no flavor dimensions. A KMP library is published as one .aar that every consumer variant resolves against, so variants of the library itself add cost without benefit. Dropping them makes configuration simpler and, according to Google, improves build performance and IDE integration.

The following table lists what disappears with variants and the documented replacement.

Feature of com.android.library Replacement in an Android-KMP library

Build types and product flavors

Create a separate standalone com.android.library module, configure variants there, and depend on it from androidMain

BuildConfig class

Generate constants for all targets with the BuildKonfig community plugin (or a similar solution)

debugImplementation for tooling

androidRuntimeClasspath (see below)

Data Binding and View Binding

Use a multiplatform UI framework such as Compose Multiplatform; these features are implementation details of a final Android app

externalNativeBuild (C/C++ via CMake)

Use Kotlin/Native cinterop in a native source set, or isolate the native code in a standalone com.android.library module

AIDL and RenderScript

Not supported; keep them in a standalone Android library module

missingDimensionStrategy, matchingFallbacks

kotlin.android.localDependencySelection {} (described below)

BuildKonfig as the BuildConfig Alternative

TrailMate wants to expose the API base URL and a debug flag to shared code on all platforms. Android’s BuildConfig cannot do that, so the shared module uses the third-party BuildKonfig plugin. This is a community plugin, not part of Kotlin or AGP; check its own documentation for the current version and options.

// sharedLogic/build.gradle.kts  (excerpt; BuildKonfig is a third-party plugin)
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.android.kotlin.multiplatform.library)
    alias(libs.plugins.buildkonfig)
}

buildkonfig {
    packageName = "com.example.trailmate"
    defaultConfigs {
        buildConfigField(Type.STRING, "API_BASE_URL", "https://api.example.com/v1/")
        buildConfigField(Type.BOOLEAN, "VERBOSE_LOGGING", "false")
    }
}

Shared code then reads BuildKonfig.API_BASE_URL from commonMain on every platform.

If the app (not the library) needs a real BuildConfig with applicationId, versionName and build-type fields, keep using it in androidApp, which still uses com.android.application and therefore still has build types and flavors. Only the shared libraries are single-variant.

Selecting Variants of Android Library Dependencies

The library itself has one variant, but it may depend on a standard Android library that has several. The kotlin.android.localDependencySelection {} block (selectBuildTypeFrom, productFlavorDimension(…​)) replaces matchingFallbacks and missingDimensionStrategy for choosing which of their variants is used.

Opt-In Features

To keep builds lean, the plugin disables everything that most shared modules do not need. Each feature is enabled explicitly inside kotlin { android { } }. The table gives the overview; the subsections show each one.

Feature Default How to enable

Java compilation

Off

withJava()

Host (local unit) tests

Off

withHostTest { }

Device (instrumented) tests

Off

withDeviceTest { }

Android resources (res/)

Off

androidResources { enable = true }

Consumer keep rules

Not published

optimization.consumerKeepRules.publish = true

Java Compilation with withJava()

If the Android target contains .java files under src/androidMain/java, call withJava(). It enables Java compilation for sources in this module only, not for its dependencies. The JVM target is set once in the target’s compilerOptions and applies to both Kotlin and Java.

// sharedLogic/build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

kotlin {
    android {
        namespace = "com.example.trailmate.shared.logic"
        compileSdk = 36
        minSdk = 24

        withJava() // compile .java files in src/androidMain/java
        compilerOptions {
            jvmTarget.set(JvmTarget.JVM_17)
        }
    }
}

The JVM target can be set at three levels, and the more specific level wins: kotlin { jvmToolchain(21) } (all JVM-based targets, lowest precedence), kotlin { android { compilerOptions { …​ } } } (all Android compilations) and the compiler options of a single compilation such as androidHostTest (highest).

The legacy separate compileOptions {} (Java) and kotlinOptions {} (Kotlin) blocks of com.android.library are replaced by this single setting.

Host and Device Tests

Android unit tests (running on the local JVM) and instrumented tests (running on a device or emulator) are both disabled by default, so the source sets androidHostTest and androidDeviceTest do not exist until you opt in. Plain multiplatform tests in commonTest do not need either: they already run as a JVM test task, and Testing shows how to organize them.

// sharedLogic/build.gradle.kts
kotlin {
    android {
        // Local JVM tests in src/androidHostTest (formerly androidUnitTest)
        withHostTest {
            isIncludeAndroidResources = true
        }
        // Instrumented tests in src/androidDeviceTest (formerly androidInstrumentedTest)
        withDeviceTest {
            instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
            execution = "HOST"
        }
    }

    sourceSets {
        androidHostTest.dependencies {
            implementation(libs.robolectric)
        }
        androidDeviceTest.dependencies {
            implementation(libs.androidx.test.runner)
        }
    }
}

The legacy configuration sets the runner in defaultConfig and the options in testOptions; with the new plugin both live in the test builder. The default directories move from src/test and src/androidTest to src/androidHostTest and src/androidDeviceTest. For Android-specific testing techniques (Robolectric, instrumented tests, Compose test rules) see Unit Testing with JUnit, Mockito and Robolectric and Instrumented and UI Testing.

Android Resources

Resource processing (res/ directories, XML drawables, strings.xml) costs build time, so the new plugin disables it. Enable it only when the module really contains Android resources, for example a vector drawable used by an Android-specific composable:

// sharedUI/build.gradle.kts
kotlin {
    android {
        namespace = "com.example.trailmate.shared.ui"
        compileSdk = 36
        minSdk = 24

        androidResources {
            enable = true
        }
    }
}

Resources then go under src/androidMain/res (not src/main/res). If the shared UI uses Compose Multiplatform resources (composeResources), you do not need Android resources for them; see Compose Multiplatform Resources.

Consumer Keep Rules

A library that needs R8/ProGuard rules to survive in its consumers' release builds ships them as consumer keep rules. With com.android.library any file listed in consumerProguardFiles was published automatically. With the new plugin publishing is explicit:

// sharedLogic/build.gradle.kts
kotlin {
    android {
        optimization {
            consumerKeepRules.apply {
                publish = true
                file("consumer-proguard-rules.pro")
            }
        }
    }
}

A common mistake is to forget publish = true after migrating, so release builds of the app crash while debug builds work. For the R8 side see Building, Signing and Releasing.

Tooling Dependencies and androidRuntimeClasspath

Compose Preview, layout inspection and similar IDE tools need androidx.compose.ui:ui-tooling at runtime, but that dependency must not leak into the published library. In a legacy module you wrote debugImplementation(…​), relying on the debug build type. A single-variant library has no debug build type, so the dependency is added to the runtime classpath of the main Android compilation instead:

// sharedUI/build.gradle.kts
dependencies {
    // Available at runtime for Compose Preview, but not on the compile classpath
    // and not part of the library's published API
    "androidRuntimeClasspath"(libs.androidx.compose.ui.tooling)
}

The configuration name is quoted because the top-level dependencies {} block has no type-safe accessor for it. The workaround is available from AGP 9.0.0-beta01. The androidApp module, which is a real application with a debug build type, keeps using debugImplementation for the same tooling.

Why AGP 9 Forces the androidApp Split

An Android application module needs everything the single-variant library model removes: build types (debug and release signing), flavors, applicationId, versionCode, manifest placeholders and the packaging pipeline that produces .apk and .aab. The Android-KMP plugin deliberately has none of that, and AGP 9 no longer lets com.android.application coexist with the Kotlin Multiplatform plugin in one module. The consequence is structural and unavoidable:

  1. The entry point (MainActivity, the manifest, launcher icons, signing, applicationId) moves to androidApp, a plain Android module with com.android.application.

  2. The shared code (sharedLogic, sharedUI) stays multiplatform and uses the Android-KMP library plugin.

  3. androidApp depends on sharedUI; the shared code never depends on the app.

This is the same pattern iOS has always had (the Xcode app depends on the Kotlin framework) and desktop and web entry points follow. It also removes a classic problem: a module that was both application and library made the Android entry point a build dependency of everything else. The recommended layout is discussed in Project Structure, Targets, Source Sets and the Hierarchy Template.

Built-In Kotlin

AGP 9.0 ships built-in Kotlin support and enables it by default: an Android module can compile Kotlin without applying org.jetbrains.kotlin.android (kotlin-android). Two consequences matter for KMP.

  • In androidApp (a plain Android module) you remove the kotlin-android plugin when you move to AGP 9. Leaving it applied fails with an error saying the plugin is no longer required.

  • In a shared module nothing changes in this respect: you still apply org.jetbrains.kotlin.multiplatform together with com.android.kotlin.multiplatform.library. Built-in Kotlin replaces kotlin-android only, and using org.jetbrains.kotlin.multiplatform together with com.android.library or com.android.application is no longer allowed once built-in Kotlin is enabled.

The androidApp build script of TrailMate Everywhere therefore looks like this: no Kotlin plugin line, the Compose compiler plugin for the UI, and a dependency on the shared UI module.

// androidApp/build.gradle.kts
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.compose.compiler) // org.jetbrains.kotlin.plugin.compose
}

android {
    namespace = "com.example.trailmate.android"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.trailmate"
        minSdk = 24
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"
    }
    buildTypes {
        release {
            isMinifyEnabled = true
            proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
        }
    }
}

kotlin {
    compilerOptions {
        jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
    }
}

dependencies {
    implementation(projects.sharedUI)
    implementation(libs.androidx.activity.compose)
    debugImplementation(libs.androidx.compose.ui.tooling)
}

The app module uses the ordinary android {} block, with src/main as its source directory, exactly as in a native Android project; that is the right place for everything described in Project Structure, Gradle and Build Variants. Other built-in-Kotlin migration items (kotlin-kapt, android.kotlinOptions {}, kotlin.sourceSets {} in androidApp) are covered in that Android page and in the migration guide linked in the references.

Migrating an Existing Project

The official recommendation is to migrate in two stages, verifying that the app runs after each, and to update AGP itself last. The steps below follow JetBrains' guide for the common case of one composeApp module that holds both the shared code and the Android entry point.

Step 1: Extract the Android App Module

  1. Create the androidApp directory with an empty build.gradle.kts and a src directory, and add include(":androidApp") to settings.gradle.kts.

  2. In androidApp/build.gradle.kts apply the application plugin and the Compose plugins, and add a dependency on the old shared module.

  3. Move the whole Android android {} block (including applicationId, targetSdk, versionCode, versionName and signing) from the shared module into androidApp.

  4. In the shared module, change com.android.application to com.android.library temporarily and delete the application-only properties from defaultConfig {}.

  5. Move src/androidMain into androidApp/src and rename it to main. Keep entry-point code (MainActivity) in androidApp, but leave all expect and actual declarations in the shared module’s source sets so that every platform sees them.

  6. Run the new androidApp run configuration. Android Studio and IntelliJ IDEA normally create it; otherwise edit the old configuration and change its module to androidApp.

Step 2: Switch the Shared Module to the Android-KMP Plugin

The legacy module used alias(libs.plugins.android.library), kotlin { androidTarget { } }, a top-level android {} block and debugImplementation(…​). After the migration it looks like this:

// sharedUI/build.gradle.kts  -- AFTER (Android-KMP library plugin)
plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.android.kotlin.multiplatform.library)
}

kotlin {
    android {
        namespace = "com.example.trailmate.shared.ui"
        compileSdk = 36
        minSdk = 24
        compilerOptions { jvmTarget.set(JvmTarget.JVM_17) }
        androidResources { enable = true } // only if the module has res/ files
    }
}

dependencies {
    "androidRuntimeClasspath"(libs.androidx.compose.ui.tooling)
}

The checklist for this stage:

  • Swap the plugin alias, and register the new plugin in the root build file with apply false.

  • Replace kotlin { androidTarget { } } with kotlin { android { } } and delete the top-level android {} block.

  • Move compileSdk and minSdk (and namespace) into the new block; drop targetSdk and applicationId.

  • Replace debugImplementation(…​) with "androidRuntimeClasspath"(…​).

  • Opt in to androidResources, withJava(), tests and keep rules wherever the module relied on the old defaults.

  • Rename test directories: src/androidUnitTest to src/androidHostTest, src/androidInstrumentedTest to src/androidDeviceTest, and legacy src/test and src/androidTest likewise.

  • Give the library a namespace that differs from the app’s namespace.

Step 3: Upgrade AGP and Gradle

Finally raise the versions: AGP to a 9.x release (this section uses 9.4.1), and the Gradle wrapper to at least the minimum that the AGP release requires (JetBrains' guide mentions 9.1.0 for AGP 9.0; this section uses Gradle 9.8.0). Remove kotlin-android from androidApp because Kotlin support is now built in, then run a full build.

The recommended order matters: doing the module split and plugin swap before bumping AGP keeps each failure attributable. If the build breaks only after the AGP bump, it is an AGP 9 issue; if it breaks after the split, it is a module wiring issue.

Escape Hatch: android.enableLegacyVariantApi

If you cannot finish the migration yet, AGP 9 can be told to keep the deprecated variant APIs alive so that the old plugin combination keeps working in the short term:

# gradle.properties
android.enableLegacyVariantApi=true

This is a stopgap, not a destination. The legacy APIs are scheduled to be removed in AGP 10, expected in the second half of 2026. Treat the flag as a deadline.

Android Studio supports AGP 9 from Otter 3 Feature Drop (2025.2.3) onward, so keep the IDE current before migrating. See Getting Started: IDEs, the KMP Plugin and Your First Project.

The androidTarget to android Rename

The name of the Android target DSL has changed twice, which causes real confusion when reading older snippets.

Kotlin version Change

Originally

An android target in the Kotlin plugin (with com.android.library), later renamed androidTarget to free the name

2.1.0

Deprecation warning raised to an error

2.2.0

android target DSL removed from the Kotlin Multiplatform Gradle plugin, so the name android is free for the new plugin

2.3.0

The new plugin is available; deprecation warning for androidTarget introduced

2.3.10

The androidTarget deprecation warning reverted

The practical reading: in a module with com.android.library, androidTarget() is still the (legacy) spelling; in a module with com.android.kotlin.multiplatform.library, the target is declared by the plugin and configured as kotlin { android { } }. Writing androidTarget() next to the new plugin is a common mistake and does not work. Existing androidTarget code on AGP 9 also needs the legacy variant API opt-in described above.

The Legacy Android Source-Set Layout Is Gone

Before Kotlin 1.9.0 the Kotlin plugin named source sets after AGP’s: androidAndroidTest, androidTest, a src/main/kotlin directory and so on. The new Android source-set layout (introduced in Kotlin 1.8.0, default since 1.9.0) names them androidMain, androidUnitTest and androidInstrumentedTest and uses src/androidMain/kotlin and src/androidMain/AndroidManifest.xml. The deprecation cycle of the old layout ended in Kotlin 2.4.0:

Kotlin version Behavior of kotlin.mpp.androidSourceSetLayoutVersion=1

1.9.0 and earlier

Warning (suppressible)

1.9.20

Error, not suppressible

2.4.0

Support for the legacy layout and the property removed

If a project that still sets that property is upgraded to Kotlin 2.4.20, delete the property and apply the rename table: main to androidMain, test to androidUnitTest, androidTest to androidInstrumentedTest, and move the manifest from src/main/AndroidManifest.xml to src/androidMain/AndroidManifest.xml. A related change: in the new layout androidInstrumentedTest no longer implicitly depends on commonTest; if you want common tests to run as instrumented tests you must add dependsOn(commonTest) yourself. (With the Android-KMP plugin these two source sets are androidHostTest and androidDeviceTest, as described above.)

Custom Gradle Plugins That React to the Android Target

Teams that centralize configuration in convention plugins (see Project Structure, Gradle and Build Variants for build-logic) must update them: the new plugin is not part of com.android.base, so checks like plugins.withId("com.android.base") no longer detect it, and its extension is not registered at the project level but lives inside the target. Google’s guide for plugin authors recommends reacting to the Kotlin Multiplatform plugin first and then to the target types:

// build-logic/convention/src/main/kotlin/TrailMateAndroidKmpConventionPlugin.kt
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 TrailMateAndroidKmpConventionPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.plugins.withId("com.android.kotlin.multiplatform.library") {
            val kmp = project.extensions.getByType(KotlinMultiplatformExtension::class.java)
            // The android {} block is a target that also acts as the Android DSL extension
            kmp.targets.withType(KotlinMultiplatformAndroidLibraryTarget::class.java).configureEach { androidTarget ->
                androidTarget.compileSdk = 36
                androidTarget.minSdk = 24
            }
        }
    }
}

Rules from the guide worth remembering:

  • Use plugins.withId(…​) rather than checking immediately, so the plugin is not sensitive to plugin application order.

  • Use targets.configureEach { } (lazily) rather than forEach, which can run before targets are registered.

  • To distinguish legacy from new integration, check target is KotlinAndroidTarget (old) versus target is KotlinMultiplatformAndroidLibraryTarget (new). The latter class was added in AGP 8.8, so guard the check with AndroidPluginVersion.getCurrent() if your plugin may run on older AGP.

  • The Android compilations of the new target are main, hostTest and deviceTest (instances of KotlinMultiplatformAndroidCompilation).

  • Do not call withHostTest {} or withDeviceTest {} just to set defaults in a convention plugin, because that eagerly creates the test components.

  • androidComponents.onVariants {} still works, but it iterates over a single variant, and build-type and flavor properties are not available on AndroidKotlinMultiplatformVariant.

For the Kotlin side of convention plugins and version catalogs in a multiplatform build, see Gradle Configuration, the Multiplatform DSL and Compatibility.

Common Mistakes

Mistake Fix

Applying com.android.library or com.android.application in a shared module on AGP 9

Use com.android.kotlin.multiplatform.library; move the app to androidApp

Keeping a top-level android {} block in a shared module

Move everything into kotlin { android { } }; there is no top-level extension

Using androidTarget() with the new plugin

Remove it; the plugin provides the target

Expecting BuildConfig, flavors or build types in a shared module

BuildKonfig for constants; a standalone Android library module for variants

debugImplementation in the shared module

"androidRuntimeClasspath"(…​)

Tests or res/ files silently ignored after migration

They are opt-in: withHostTest, withDeviceTest, androidResources

Release crashes after migration because keep rules vanished

Set optimization.consumerKeepRules.publish = true

Leaving org.jetbrains.kotlin.android in androidApp on AGP 9

Remove it; Kotlin is built in