assets/checklist.md
# KMP AGP 9.0 Migration Verification Checklist
Use this checklist after migration to verify everything is configured correctly.
## Plugin Configuration
- [ ] `com.android.kotlin.multiplatform.library` plugin declared for KMP library modules
- [ ] No `com.android.library` or `com.android.application` in KMP modules' build.gradle.kts
- [ ] `org.jetbrains.kotlin.android` removed from all build files and version catalog (built-in Kotlin replaces it)
- [ ] No `org.jetbrains.kotlin.kapt` plugin — migrated to KSP or `com.android.legacy-kapt`
- [ ] `android.kotlinOptions {}` migrated to `kotlin { compilerOptions {} }` (non-KMP modules)
- [ ] `kotlin.sourceSets` migrated to `android.sourceSets` with `.kotlin` accessor (non-KMP modules)
- [ ] No `android.builtInKotlin=false` unless required by incompatible plugin (documented as temporary)
- [ ] Third-party plugins verified compatible
## KMP Library Modules
- [ ] Source sets renamed: `androidMain`, `androidHostTest`, `androidDeviceTest`
- [ ] No `android {}` top-level block — use `androidLibrary {}` inside `kotlin {}` instead
- [ ] `androidResources { enable = true }` present if module uses Android or Compose Multiplatform resources
- [ ] `withJava()` present if module has .java source files
- [ ] Tests configured: `withHostTest {}`, `withDeviceTest {}`
- [ ] No `debugImplementation` or analogs in library modules
- use `androidRuntimeClasspath` for tooling deps
- app modules can still use `debugImplementation`
- [ ] Unique `namespace` for each library module (different from app module; `android.uniquePackageNames=true` is default in AGP 9.0)
## Gradle Properties & DSL
- [ ] No removed properties in `gradle.properties` that cause errors:
- `android.enableLegacyVariantApi`
- `android.r8.integratedResourceShrinking`
- `android.enableNewResourceShrinker.preciseShrinking`
- [ ] Any opt-out flags (`android.newDsl=false`, `android.builtInKotlin=false`) documented with reason
- [ ] `targetSdk` explicitly set in all app modules (defaults to `compileSdk` now, was `minSdk`)
## Build Logic / Convention Plugins
- [ ] No references to `BaseExtension`, `AppExtension`, `LibraryExtension` (removed in AGP 9.0)
- [ ] Using `CommonExtension` or specific new DSL types
- [ ] No use of removed APIs: `applicationVariants`, `libraryVariants`, `variantFilter`
## ProGuard / R8
- [ ] Consumer ProGuard rules migrated to `consumerProguardFiles.add(file(...))` in new DSL
- [ ] Using `proguard-android-optimize.txt` (not `proguard-android.txt`)
- [ ] No global options (`-dontobfuscate`, `-dontoptimize`) in library consumer rules
- [ ] Keep rules updated for R8 strict full mode (explicit default constructor rules if needed)
## Build & Test Verification
- [ ] `./gradlew build` succeeds
- [ ] `./gradlew :androidApp:assembleDebug` succeeds (if app module exists)
- [ ] `xcodebuild -project iosApp/*.xcodeproj -scheme <scheme> -sdk iphonesimulator build` succeeds (if iOS app exists)
- [ ] Desktop app compiles: `./gradlew :desktopApp:run` or equivalent (if desktop target exists)
- [ ] Web/Wasm target compiles: `./gradlew :wasmJsApp:wasmJsBrowserDistribution` or equivalent (if web target exists)
- [ ] `./gradlew :shared:allTests` succeeds (or equivalent for KMP test tasks)
- [ ] `./gradlew :androidApp:testDebugUnitTest` succeeds (if app module exists)
- [ ] No deprecation warnings about variant API or DSL
references/DSL-REFERENCE.md
# DSL Reference: AGP 8.x to AGP 9.x KMP Library Migration
Side-by-side mapping of every DSL element from the old `com.android.library` configuration to the new `com.android.kotlin.multiplatform.library` configuration.
---
## Plugin IDs
| Old (AGP 8.x) | New (AGP 9.x) |
|--------------------------------------|-----------------------------------------------------------------------------------------------------|
| `com.android.library` | `com.android.kotlin.multiplatform.library` |
| `com.android.application` | `com.android.application` (unchanged, but cannot combine with KMP) |
| `org.jetbrains.kotlin.android` | Built into `com.android.application` and `com.android.library` in AGP 9.0 (do not apply separately) |
| `org.jetbrains.kotlin.kapt` | `com.android.legacy-kapt` (same version as AGP) or migrate to KSP |
| `org.jetbrains.kotlin.multiplatform` | `org.jetbrains.kotlin.multiplatform` (unchanged) |
---
## Top-Level Block Migration
| Old | New |
|-------------------------------------------|-------------------------------------|
| `android { ... }` | `kotlin { android { ... } }` |
| `androidTarget { ... }` (in kotlin block) | `android { ... }` (in kotlin block) |
---
## android {} Block Fields
### Namespace and SDK Versions
| Old (android {}) | New (kotlin { android {} }) |
|------------------------------------|-------------------------------------------------------------------------|
| `namespace = "..."` | `namespace = "..."` |
| `compileSdk = 35` | `compileSdk = 35` (same value, just moved into `kotlin { android {} }`) |
| `defaultConfig { minSdk = 24 }` | `minSdk = 24` |
| `defaultConfig { targetSdk = 34 }` | N/A (application-only, not in library) |
### defaultConfig Elements
| Old (android { defaultConfig {} }) | New (kotlin { android {} }) |
|--------------------------------------------|-------------------------------------------|
| `minSdk = 24` | `minSdk = 24` (direct property) |
| `testInstrumentationRunner = "..."` | Set in `withDeviceTest { }` configuration |
| `consumerProguardFiles("...")` | `consumerProguardFiles.add(file("..."))` |
| `multiDexEnabled = true` | N/A (handled automatically) |
| `vectorDrawables.useSupportLibrary = true` | N/A |
| `buildConfigField(...)` | Removed (see KNOWN-ISSUES.md) |
| `manifestPlaceholders[...]` | N/A (use merged manifest in app module) |
---
## Compile Options and Compiler Options
| Old | New |
|-------------------------------------------------------------------------------|------------------------------------------------------------------------------|
| `android { compileOptions { sourceCompatibility = JavaVersion.VERSION_11 } }` | `kotlin { android { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } }` |
| `android { compileOptions { targetCompatibility = JavaVersion.VERSION_11 } }` | `kotlin { android { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } }` |
| `kotlinOptions { jvmTarget = "11" }` | `compilerOptions { jvmTarget.set(JvmTarget.JVM_11) }` |
| `kotlinOptions { freeCompilerArgs += listOf("-Xopt-in=...") }` | `compilerOptions { optIn.add("...") }` |
| `kotlinOptions { languageVersion = "1.9" }` | `compilerOptions { languageVersion.set(KotlinVersion.KOTLIN_2_0) }` |
Full JvmTarget import:
```kotlin
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
```
---
## Build Features
| Old (android { buildFeatures {} }) | New (kotlin { android {} }) |
|-----------------------------------------|------------------------------------------------------------------------------|
| `buildFeatures { compose = true }` | Applied via compose compiler plugin (no explicit flag needed in KMP library) |
| `buildFeatures { buildConfig = true }` | Removed in KMP library (see KNOWN-ISSUES.md) |
| `buildFeatures { viewBinding = true }` | Not supported in KMP library |
| `buildFeatures { dataBinding = true }` | Not supported in KMP library |
| `buildFeatures { aidl = true }` | Not supported in KMP library |
| `buildFeatures { renderScript = true }` | Not supported |
| `buildFeatures { resValues = true }` | Not supported in KMP library |
---
## Android Resources
| Old | New |
|---------------------------------------------|-------------------------------------------------------------|
| Resources processed by default | Must explicitly enable |
| `android { ... }` (resources auto-included) | `kotlin { android { androidResources { enable = true } } }` |
---
## Test Options
| Old | New |
|----------------------------------------------------------------------|--------------------------------------------------------------------------------|
| `android { testOptions { unitTests.isReturnDefaultValues = true } }` | `kotlin { android { withHostTest { } } }` |
| `android { testOptions { animationsDisabled = true } }` | `kotlin { android { withDeviceTest { } } }` |
| Source set: `androidUnitTest` | Source set: `androidHostTest` (alias: `androidUnitTest` still works) |
| Source set: `androidInstrumentedTest` | Source set: `androidDeviceTest` (alias: `androidInstrumentedTest` still works) |
| `testImplementation(...)` | `getByName("androidHostTest").dependencies { implementation(...) }` |
| `androidTestImplementation(...)` | `getByName("androidDeviceTest").dependencies { implementation(...) }` |
| Source dir: `src/test/` | Source dir: `src/androidHostTest/kotlin/` |
| Source dir: `src/androidTest/` | Source dir: `src/androidDeviceTest/kotlin/` |
### Test Configuration Details
```kotlin
// Old
android {
testOptions {
unitTests {
isReturnDefaultValues = true
isIncludeAndroidResources = true
}
}
}
// New
kotlin {
android {
withHostTest {
// Host test specific configuration
// returnDefaultValues and includeAndroidResources
// are configured via gradle.properties or test runner
}
withDeviceTest {
// Device test specific configuration
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
}
```
---
## Lint Configuration
| Old | New |
|-------------------------------------------------------------|------------------------------------------------------------------------|
| `android { lint { abortOnError = false } }` | `kotlin { android { lint { abortOnError = false } } }` |
| `android { lint { checkReleaseBuilds = true } }` | `kotlin { android { lint { checkReleaseBuilds = true } } }` |
| `android { lint { disable += "SomeCheck" } }` | `kotlin { android { lint { disable += "SomeCheck" } } }` |
| `android { lint { baseline = file("lint-baseline.xml") } }` | `kotlin { android { lint { baseline = file("lint-baseline.xml") } } }` |
The lint DSL is largely unchanged, it just moves inside `kotlin { android {} }`.
**Note:** `useK2Uast` is deprecated. Remove it if present.
---
## Packaging / Resources Excludes
| Old | New |
|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| `android { packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } }` | `kotlin { android { packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } } } }` |
The DSL is the same, just nested under `kotlin { android {} }`.
**Syntax note for AGP 9.0:**
```kotlin
// Old syntax (still works but deprecated)
resources.excludes += "/META-INF/{AL2.0,LGPL2.1}"
// Preferred AGP 9.0 syntax
resources {
excludes.add("/META-INF/AL2.0")
excludes.add("/META-INF/LGPL2.1")
}
```
---
## Dependencies Configurations
| Old Configuration | New Configuration | Notes |
|----------------------------------|-----------------------------------------------------------------------|-----------------------------|
| `implementation(...)` | `androidMain.dependencies { implementation(...) }` | Move to source set |
| `api(...)` | `androidMain.dependencies { api(...) }` | Move to source set |
| `compileOnly(...)` | `androidMain.dependencies { compileOnly(...) }` | Move to source set |
| `debugImplementation(...)` | `"androidRuntimeClasspath"(...)` | No variant-specific configs |
| `releaseImplementation(...)` | `androidMain.dependencies { implementation(...) }` | Single variant |
| `testImplementation(...)` | `getByName("androidHostTest").dependencies { implementation(...) }` | |
| `androidTestImplementation(...)` | `getByName("androidDeviceTest").dependencies { implementation(...) }` | |
| `ksp(...)` | `add("ksp", ...)` or KSP Gradle plugin DSL | Check KSP compatibility |
| `kapt(...)` | Migrate to KSP; kapt not supported | |
---
## Dependency Resolution
Because the new KMP Android library plugin is strictly single-variant, you can no longer define fallback logic inside `buildTypes` or `defaultConfig`.
| Old | New | Notes |
|---|---|---|
| `android { defaultConfig { missingDimensionStrategy("tier", "free") } }` | `kotlin { android { localDependencySelection { productFlavorDimension("tier") { selectFrom.set(listOf("free")) } } } }` | Configure dependency flavor fallbacks |
| `android { buildTypes { getByName("debug") { matchingFallbacks.add("release") } } }` | `kotlin { android { localDependencySelection { selectBuildTypeFrom.set(listOf("debug", "release")) } } }` | Configure dependency build type mapping |
---
## Build Types and Product Flavors
**Removed in KMP library plugin.** The `com.android.kotlin.multiplatform.library` plugin produces a single build variant.
| Old | New | Notes |
|---------------------------------------------------|---------|-----------------------------------------------------|
| `buildTypes { debug { ... } }` | Removed | Single variant only |
| `buildTypes { release { minifyEnabled = true } }` | Removed | Minification is app-module concern |
| `productFlavors { ... }` | Removed | Use Gradle properties or expect/actual for variants |
| `flavorDimensions(...)` | Removed | |
### Workarounds for Variant-Dependent Logic
1. **Compile-time constants:** Use `expect`/`actual` or dependency injection instead of `BuildConfig`.
2. **Environment-specific behavior:** Use Gradle properties or runtime configuration.
3. **Different dependencies per build type:** Not possible in KMP library. Move to app module.
4. **Minification/ProGuard:** Only relevant in the application module.
---
## androidComponents Block
| Old | New |
|------------------------------------------------|--------------------------------------------------|
| `androidComponents { onVariants { ... } }` | Limited support; most variant API is unavailable |
| `androidComponents { beforeVariants { ... } }` | Not available in KMP library |
| `androidComponents { finalizeDsl { ... } }` | Not available in KMP library |
The `androidComponents` extension is significantly reduced in scope for KMP libraries because there is only a single variant. Most customization that relied on variant-aware APIs must be reworked.
**Note:** `android.enableLegacyVariantApi` is **removed** in AGP 9.0 and will cause an error if set. Code depending on legacy variant APIs must be migrated to `androidComponents` APIs.
---
## Java Source Compilation
| Old | New |
|---------------------------------------------------------|-------------------------------------|
| Java sources in `src/main/java/` compiled automatically | Must call `withJava()` |
| `android { compileOptions { ... } }` | `kotlin { android { withJava() } }` |
```kotlin
kotlin {
android {
withJava() // Required to compile .java files in androidMain
}
}
```
---
## Quick Reference: Minimal Migration Template
```kotlin
// OLD
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
kotlin {
androidTarget { compilations.all { kotlinOptions { jvmTarget = "11" } } }
}
android {
namespace = "com.example.lib"
compileSdk = 35
defaultConfig { minSdk = 24 }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
// NEW
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKmpLibrary)
}
kotlin {
android {
namespace = "com.example.lib"
compileSdk = 35
minSdk = 24
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
}
```
references/KNOWN-ISSUES.md
# Known Issues: KMP AGP 9.0 Library Migration
Comprehensive list of gotchas, limitations, and workarounds when migrating to `com.android.kotlin.multiplatform.library`. Based on official Android documentation and community experience.
---
## 1. BuildConfig Removed in Libraries
**Problem:** The `BuildConfig` class is not generated for KMP library modules. Code referencing `BuildConfig.DEBUG`, `BuildConfig.VERSION_NAME`, or custom `buildConfigField` entries will fail to compile.
**Impact:** High. Many libraries use `BuildConfig.DEBUG` for logging gates and `buildConfigField` for compile-time constants.
**Workaround -- AppConfiguration DI Pattern:**
```kotlin
// In commonMain
expect class AppConfiguration {
val isDebug: Boolean
val versionName: String
val apiBaseUrl: String
}
// In androidMain
actual class AppConfiguration(private val context: Context) {
actual val isDebug: Boolean = (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE) != 0
actual val versionName: String = context.packageManager
.getPackageInfo(context.packageName, 0).versionName ?: "unknown"
actual val apiBaseUrl: String = if (isDebug) "https://dev.api.example.com" else "https://api.example.com"
}
// In iosMain
actual class AppConfiguration {
actual val isDebug: Boolean = Platform.isDebugBinary
actual val versionName: String = NSBundle.mainBundle.objectForInfoDictionaryKey("CFBundleShortVersionString") as? String ?: "unknown"
actual val apiBaseUrl: String = if (isDebug) "https://dev.api.example.com" else "https://api.example.com"
}
```
Inject `AppConfiguration` via your DI framework (Koin, kotlin-inject, manual DI).
**Alternative A — BuildKonfig plugin** ([github.com/yshrsmz/BuildKonfig](https://github.com/yshrsmz/BuildKonfig)):
Generates `expect`/`actual` BuildConfig objects across all KMP targets. Supports typed fields
(String, Int, Long, Float, Boolean), target-specific overrides, and a flavor system via Gradle properties.
```kotlin
// build.gradle.kts
plugins {
id("com.codingfeline.buildkonfig")
}
buildkonfig {
packageName = "com.example.shared"
defaultConfigs {
buildConfigField(STRING, "API_BASE_URL", "https://api.example.com")
buildConfigField(BOOLEAN, "IS_DEBUG", "false")
buildConfigField(STRING, "VERSION_NAME", "1.0.0")
}
// Optional: target-specific overrides
targetConfigs {
create("android") {
buildConfigField(STRING, "PLATFORM", "android")
}
create("ios") {
buildConfigField(STRING, "PLATFORM", "ios")
}
}
}
```
Use flavors for debug/release by setting `buildkonfig.flavor=dev` in `gradle.properties`
or passing `-Pbuildkonfig.flavor=release` on CLI:
```kotlin
defaultConfigs("dev") {
buildConfigField(STRING, "API_BASE_URL", "https://dev.api.example.com")
buildConfigField(BOOLEAN, "IS_DEBUG", "true")
}
defaultConfigs("release") {
buildConfigField(STRING, "API_BASE_URL", "https://api.example.com")
buildConfigField(BOOLEAN, "IS_DEBUG", "false")
}
```
**Alternative B — gradle-buildconfig-plugin** ([github.com/gmazzo/gradle-buildconfig-plugin](https://github.com/gmazzo/gradle-buildconfig-plugin)):
More general-purpose; supports Java, Kotlin, Groovy, and KMP. Richer type support (arrays, maps,
Files, URIs). Uses `expect`/`actual` for KMP via explicit `expect()` calls.
```kotlin
// build.gradle.kts
plugins {
id("com.github.gmazzo.buildconfig")
}
buildConfig {
packageName("com.example.shared")
buildConfigField("APP_NAME", project.name)
buildConfigField("VERSION", "1.0.0")
buildConfigField("IS_DEBUG", false)
// Platform-specific fields using expect/actual
buildConfigField("PLATFORM", expect<String>())
}
// In source set configurations:
sourceSets.named("androidMain") {
buildConfigField("PLATFORM", "android")
}
sourceSets.named("iosMain") {
buildConfigField("PLATFORM", "ios")
}
```
**Important limitation:** Neither plugin replaces Android build variants fully. They provide
compile-time constants only. Build type-specific dependencies, resources, source sets, signing
configs, and minification settings must be handled in the application module (which still supports
variants) or via runtime configuration.
---
## 2. NDK / JNI Unsupported
**Problem:** The KMP library plugin does not support `externalNativeBuild`, `ndkVersion`, or JNI source compilation. Modules that use C/C++ native code via NDK cannot be migrated directly.
**Impact:** Medium. Affects modules with native image processing, crypto, or media libraries.
**Workaround -- Proxy Interface Pattern:**
Keep the JNI module as a classic `com.android.library` module and have the KMP module depend on it:
```
jni-bridge/ # com.android.library (AGP 8.x compatible in AGP 9.0)
build.gradle.kts
src/main/jni/ # C/C++ sources
src/main/kotlin/ # JNI bindings
shared/ # com.android.kotlin.multiplatform.library
build.gradle.kts
```
```kotlin
// shared/build.gradle.kts
kotlin {
sourceSets {
androidMain.dependencies {
implementation(project(":jni-bridge"))
}
}
}
```
Define an interface in `commonMain` and implement it in `androidMain` by delegating to the JNI bridge.
---
## 3. No Build Variants
**Problem:** The KMP library plugin produces a single build variant. There are no `debug`/`release` build types and no product flavors. Code that depends on variant-specific behavior, resources, or dependencies must be restructured.
**Impact:** High. Affects projects using flavor-specific dependencies, resources, or source sets.
**Workaround -- Single Variant Architecture:**
- Move all variant-dependent logic to the application module (which still supports variants).
- Use runtime configuration instead of compile-time variants.
- Use `expect`/`actual` with different actual implementations selected by DI based on runtime config.
- For library-specific debug/release behavior, use the `AppConfiguration` pattern from issue 1.
- For compile-time constants that vary by build flavor, use **BuildKonfig** or **gradle-buildconfig-plugin** (see issue 1 alternatives). These provide a flavor-like system for KMP but do NOT replace variant-specific dependencies, resources, signing, or minification.
---
## 4. Compose Resources Require Explicit Enable
**Problem:** Android resources (`res/` directory) are not processed by default with the KMP library plugin. If you forget to enable them, resource references (`R.string.*`, `R.drawable.*`) will fail to resolve. This is tracked as CMP-9547.
**Impact:** High. Silent failure -- resources are simply ignored without an error until you try to reference them.
**Fix:**
```kotlin
kotlin {
android {
androidResources { enable = true }
}
}
```
**Note:** This is separate from Compose Multiplatform resources (`composeResources/`), which are handled by the compose resources plugin and do not need this flag.
---
## 5. Consumer ProGuard Rules Silently Dropped
**Problem:** If you had `consumerProguardFiles` in the old `android { defaultConfig {} }` block and did not migrate it to the new DSL location, the rules are silently ignored. No warning is emitted.
**Impact:** Medium. Can cause runtime crashes in release builds of consuming applications.
**Fix:**
```kotlin
// Old (silently ignored)
android {
defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}
}
// New
kotlin {
android {
consumerProguardFiles.add(file("consumer-rules.pro"))
}
}
```
---
## 6. Convention Plugin Refactoring Needed
**Problem:** Build-logic convention plugins that apply `com.android.library` and configure the `LibraryExtension` must be rewritten to use the KMP library plugin and `KotlinMultiplatformExtension`.
**Impact:** Medium to High for projects with extensive build-logic modules.
**Key Changes:**
```kotlin
// Old
import com.android.build.gradle.LibraryExtension
class MyConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply("com.android.library")
target.extensions.configure<LibraryExtension> {
compileSdk = 34
defaultConfig.minSdk = 24
}
}
}
// New
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
class MyConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.pluginManager.apply("org.jetbrains.kotlin.multiplatform")
target.pluginManager.apply("com.android.kotlin.multiplatform.library")
target.extensions.configure<KotlinMultiplatformExtension> {
android {
compileSdk = 35
minSdk = 24
}
}
}
}
```
---
## 7. Renamed test source sets
**Problem:** The source set `androidUnitTest` is renamed to `androidHostTest`. The source set `androidInstrumentedTest` is renamed to `androidDeviceTest`. The old names still work as aliases but are deprecated.
**Impact:** Low. Aliases provide backward compatibility, but you should rename for clarity.
**Action Items:**
- Rename `src/androidUnitTest/` to `src/androidHostTest/`
- Rename `src/androidInstrumentedTest/` to `src/androidDeviceTest/`
- Update `sourceSets` references in `build.gradle.kts`
- Update CI scripts that reference the old directory names
---
## 8. Lint useK2Uast Deprecated
**Problem:** The `lint { useK2Uast = true }` option is deprecated. With KGP 2.0+ and AGP 9.0, K2 UAST is the default and only implementation.
**Impact:** Low. Build warning only.
**Fix:** Remove the line:
```kotlin
// Remove this
lint {
useK2Uast = true // DELETE
}
```
---
## 9. Packaging Exclusions Syntax Change
**Problem:** The packaging exclusions DSL has a subtle syntax difference. The old brace-expansion syntax may not work correctly.
**Impact:** Low. Build may fail or produce unexpected results.
**Fix:**
```kotlin
// Old (may not work correctly in AGP 9.0)
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
// New (explicit entries)
packaging {
resources {
excludes.add("/META-INF/AL2.0")
excludes.add("/META-INF/LGPL2.1")
}
}
```
---
## 10. Static BuildConfig.DEBUG for Tree-Shaking No Longer Available
**Problem:** In classic Android libraries, `BuildConfig.DEBUG` was a `static final boolean` that the compiler could use for dead-code elimination (tree-shaking). Without BuildConfig in KMP libraries, this optimization path is lost.
**Impact:** Low to Medium. Debug-only code paths may be included in release builds.
**Workaround:**
Use R8 rules in the application module to remove debug code:
```proguard
# In the app module's proguard-rules.pro
-assumenosideeffects class com.example.shared.AppConfiguration {
boolean isDebug() return false;
}
```
Or use compile-time constants from the application module passed via DI.
---
## 11. android.builtInKotlin=false (Temporary Opt-Out)
**Problem:** AGP 9.0 bundles Kotlin compilation for `com.android.application` modules, meaning you should NOT apply the `kotlin-android` plugin separately. If you have tooling or plugins that conflict with the built-in Kotlin support, you can temporarily disable it.
**Temporary Workaround:**
```properties
# gradle.properties
android.builtInKotlin=false
```
**Warning:** This flag is temporary and will be removed in a future AGP version. Use it only to unblock migration while you fix plugin conflicts.
**When you need this:**
- You have custom Kotlin compiler plugin configurations that conflict with AGP's built-in setup
- A third-party Gradle plugin explicitly applies `kotlin-android` and conflicts
---
## 12. IDE Support Matrix
| IDE | KMP Library Plugin Support | Notes |
|---|---|-----------------------------------------------------------|
| Android Studio Otter 3 (2025.2.3) | Full | First version with full AGP 9.0 support |
| Android Studio Narwhal (2025.1) | Partial | Can build but limited code insight for new DSL |
| Android Studio Meerkat (2024.3) | None | Does not recognize the KMP library plugin |
| IntelliJ IDEA | Not supported | Does not support AGP 9.0 as of 2025.3, use Android Studio |
### Recommended IDE Setup
- **Use Android Studio Otter 3 or later.** This is currently the only IDE with full AGP 9.0 + KMP library plugin support.
- **IntelliJ IDEA does not support AGP 9.0** — even with the Android plugin installed, project sync and build integration will not work correctly. If the project uses IntelliJ IDEA, recommend switching to Android Studio for Android/KMP development.
- Ensure the Kotlin plugin in your IDE matches the KGP version in your project.
---
## 13. kapt Incompatible with Built-in Kotlin
**Problem:** The `org.jetbrains.kotlin.kapt` plugin is incompatible with AGP 9.0's built-in Kotlin support. Applying both causes a build failure.
**Impact:** High. Many projects still use kapt for annotation processing
**Workaround:**
**Preferred:** Migrate to KSP (Kotlin Symbol Processing). Most annotation processors now support KSP.
**Fallback:** Replace with `com.android.legacy-kapt` (versioned with AGP):
```toml
# gradle/libs.versions.toml
[plugins]
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }
```
```kotlin
// Module build.gradle.kts
plugins {
// REMOVE: alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.legacy.kapt)
}
```
---
## 14. New DSL Interfaces (BaseExtension Removed)
**Problem:** AGP 9.0 exclusively uses new public DSL interfaces. The old `BaseExtension`, `AppExtension`, `LibraryExtension` types from `com.android.build.gradle` are removed. Build logic or convention plugins casting to these types will fail with `ClassCastException`.
**Impact:** High for projects with custom build logic or convention plugins.
**Error message:**
```
java.lang.ClassCastException: class com.android.build.gradle.internal.dsl.ApplicationExtensionImpl$AgpDecorated_Decorated
cannot be cast to class com.android.build.gradle.BaseExtension
```
**Fix:**
```kotlin
// Old
import com.android.build.gradle.BaseExtension
val ext = extensions.getByType(BaseExtension::class)
// New
import com.android.build.api.dsl.CommonExtension
val ext = extensions.getByType(CommonExtension::class)
```
**Temporary opt-out:** `android.newDsl=false` in `gradle.properties` (removed in AGP 10.0).
---
## 15. Deprecated Variant APIs Removed
**Problem:** The following APIs are removed in AGP 9.0: `applicationVariants`, `libraryVariants`, `testVariants`, `unitTestVariants`, `variantFilter`. Build scripts or plugins using these will fail.
**Impact:** Medium-High. Affects custom build logic and many third-party plugins.
**Fix:**
```kotlin
// Old
android {
applicationVariants.all { variant ->
variant.signingConfig.enableV1Signing = false
}
}
// New
androidComponents {
onVariants { variant ->
variant.signingConfig.enableV1Signing.set(false)
}
}
```
Replace `variantFilter` with `androidComponents.beforeVariants()`.
---
## 16. R8 and ProGuard Rule Changes
**Problem:** AGP 9.0 changes several R8 defaults:
- `android.r8.strictFullModeForKeepRules=true` — keep rules no longer implicitly keep default constructors
- `android.r8.proguardAndroidTxt.disallowed=true` — only `proguard-android-optimize.txt` is supported
- `android.r8.globalOptionsInConsumerRules.disallowed=true` — library consumer rules cannot contain global options (like `-dontobfuscate`)
- Keep rules no longer propagate to synthesized companion methods
**Impact:** Medium. Release builds may crash or behave differently without rule updates.
**Fix:**
- Review all ProGuard/R8 keep rules; add explicit rules for default constructors if needed
- Switch to `proguard-android-optimize.txt` in `getDefaultProguardFile()`
- Remove global options (`-dontobfuscate`, `-dontoptimize`) from library consumer rules
- New option: `-processkotlinnullchecks keep|remove_message|remove` to control Kotlin null checks
---
## 17. Removed Features
**Problem:** Several features are removed in AGP 9.0 with no replacement:
- **Embedded Wear OS app support** — `wearApp` configurations removed
- **Density split APK** — use app bundles instead
- **`androidDependencies` and `sourceSets` report tasks** — removed
- **`dexOptions` DSL** — removed (d8 handles this automatically)
- **RenderScript** — disabled by default, enable per-module if needed: `buildFeatures { renderScript = true }`
- **AIDL** — disabled by default, enable per-module if needed: `buildFeatures { aidl = true }`
**Impact:** Low-Medium. Only affects projects using these specific features.
---
## 18. R Class Non-Final in Application Modules
**Problem:** AGP 9.0 makes R class fields compile-time non-final in application modules (`android.enableAppCompileTimeRClass=true` is now default). Code using `switch` statements on R class fields (like `R.id.some_view`) will fail to compile because `switch` requires compile-time constants.
**Impact:** Medium. Common in older Java codebases using `switch(view.getId())`.
**Fix:** Refactor `switch` statements to `if/else`:
```java
// Old (fails with AGP 9.0)
switch (view.getId()) {
case R.id.button1: // ...
case R.id.button2: // ...
}
// New
int id = view.getId();
if (id == R.id.button1) { /* ... */ }
else if (id == R.id.button2) { /* ... */ }
```
---
## 19. targetSdk Defaults to compileSdk
**Problem:** AGP 9.0 changes `targetSdk` to default to `compileSdk` when not explicitly set (previously defaulted to `minSdk`). This can silently change app behavior if `targetSdk` was intentionally unset.
**Impact:** Medium. May trigger new runtime behavior changes associated with higher API levels.
**Fix:** Explicitly set `targetSdk` in all application modules:
```kotlin
android {
defaultConfig {
targetSdk = 35 // Set explicitly
}
}
```
---
## 20. Third-Party Plugin Compatibility
**Problem:** Many third-party Gradle plugins are incompatible with AGP 9.0 due to removed variant APIs, new DSL interfaces, or built-in Kotlin conflicts. See the main SKILL.md "Plugin Compatibility" section for the full compatibility table.
**Impact:** High. Can completely block migration.
**Key plugins requiring opt-out flags:**
- detekt < 2.0.0, ktlint, SQLDelight, Paparazzi, protobuf — see SKILL.md for specific flags
---
references/MIGRATION-APP-SPLIT.md
# Splitting a KMP + Android Application Module for AGP 9.0
AGP 9.0 does not support `com.android.application` combined with `org.jetbrains.kotlin.multiplatform` in the same module. You must split the monolithic `composeApp` module into a pure Android application module and a KMP shared library module.
---
## Old Structure (AGP 8.x)
```
composeApp/
build.gradle.kts # com.android.application + kotlin.multiplatform
src/
commonMain/kotlin/ # Shared KMP code
androidMain/kotlin/ # Android-specific code + MainActivity
androidMain/res/ # Android resources
androidMain/AndroidManifest.xml
iosMain/kotlin/ # iOS-specific code
desktopMain/kotlin/ # Desktop entry point (optional)
```
Single `composeApp/build.gradle.kts`:
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions { jvmTarget = "11" }
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(libs.kotlinx.coroutines.core)
}
androidMain.dependencies {
implementation(libs.androidx.activity.compose)
implementation(libs.compose.ui.tooling.preview)
}
}
}
android {
namespace = "com.example.app"
compileSdk = 34
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildFeatures { compose = true }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
debugImplementation(libs.compose.ui.tooling)
}
```
---
## New Structure (AGP 9.x)
```
shared/
build.gradle.kts # kotlin.multiplatform + com.android.kotlin.multiplatform.library
src/
commonMain/kotlin/ # All shared KMP code
androidMain/kotlin/ # Android-specific implementations (expect/actual)
androidMain/res/ # Shared Android resources (if any)
iosMain/kotlin/ # iOS-specific code
androidApp/
build.gradle.kts # com.android.application ONLY (no kotlin.multiplatform)
src/
main/kotlin/ # MainActivity, Application class
main/res/ # App-level resources (launcher icons, themes, etc.)
main/AndroidManifest.xml # Full manifest with <application> and <activity>
iosApp/ # Unchanged
```
---
## androidApp/build.gradle.kts
**Important:** In AGP 9.0, the `com.android.application` plugin has Kotlin support built in. Do NOT apply `org.jetbrains.kotlin.android` separately -- it will conflict.
```kotlin
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeCompiler)
}
android {
namespace = "com.example.app"
compileSdk = 35
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildFeatures {
compose = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
dependencies {
implementation(project(":shared"))
implementation(libs.androidx.activity.compose)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)
}
```
### Key Points for androidApp
- **No `kotlin.multiplatform` plugin.** This is a pure Android application module.
- **No `kotlin-android` plugin.** AGP 9.0's `com.android.application` plugin bundles Kotlin compilation. Applying `org.jetbrains.kotlin.android` will cause a conflict error.
- **`buildTypes` and `productFlavors` work here.** The application plugin still supports full variant configuration.
- **Compose compiler plugin** is applied separately (`composeCompiler`), or it can come from KGP 2.0+ if you use the compose compiler Gradle plugin.
- **Depends on `:shared`** to access all shared KMP code.
---
## shared/build.gradle.kts
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKmpLibrary)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
android {
namespace = "com.example.shared"
compileSdk = 35
minSdk = 24
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
androidResources { enable = true }
}
iosX64()
iosArm64()
iosSimulatorArm64()
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "Shared"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(libs.kotlinx.coroutines.core)
}
androidMain.dependencies {
// Android-specific shared dependencies only
}
}
}
```
### Key Points for shared
- **Plugin is `com.android.kotlin.multiplatform.library`**, not `com.android.library`.
- **Namespace must differ from androidApp.** Use `com.example.shared` vs `com.example.app`.
- **No `applicationId`, `versionCode`, `versionName`, `targetSdk`.** These are application-only concepts.
- **No `buildTypes` or `productFlavors`.** The KMP library plugin produces a single variant.
- **Framework exports** (`binaries.framework`) stay here since iOS depends on the shared module.
- **`androidTarget {}`** is replaced with **`android {}`**.
---
## settings.gradle.kts Changes
### Before
```kotlin
rootProject.name = "MyProject"
include(":composeApp")
include(":iosApp") // if present as a Gradle module
```
### After
```kotlin
rootProject.name = "MyProject"
include(":shared")
include(":androidApp")
include(":iosApp")
```
---
## Root build.gradle.kts Changes
### With Version Catalog
**Before:**
```kotlin
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
}
```
**After:**
```kotlin
plugins {
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidKmpLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
}
```
### Without Version Catalog
**Before:**
```kotlin
plugins {
id("com.android.application") version "8.7.3" apply false
id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.1.0" apply false
id("org.jetbrains.compose") version "1.7.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
}
```
**After:**
```kotlin
plugins {
id("com.android.application") version "9.0.1" apply false
id("com.android.kotlin.multiplatform.library") version "9.0.1" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.3.20" apply false
id("org.jetbrains.compose") version "1.10.3" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.3.20" apply false
}
```
Note: `com.android.library` is replaced with `com.android.kotlin.multiplatform.library`. If you still have pure Android library modules (non-KMP), you can keep `com.android.library` as well.
---
## What to Move to androidApp
These items are Android application concerns and must move out of the shared KMP module:
### 1. MainActivity (and any other Activities)
```
composeApp/src/androidMain/kotlin/com/example/app/MainActivity.kt
--> androidApp/src/main/kotlin/com/example/app/MainActivity.kt
```
Update `MainActivity` to call into shared code:
```kotlin
// androidApp/src/main/kotlin/com/example/app/MainActivity.kt
package com.example.app
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.example.shared.App // Import from shared module
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
App() // Shared composable
}
}
}
```
### 2. AndroidManifest.xml
The full application manifest with `<application>`, `<activity>`, `<intent-filter>` moves to androidApp:
```
composeApp/src/androidMain/AndroidManifest.xml
--> androidApp/src/main/AndroidManifest.xml
```
**Important:** After moving the manifest, verify the `android:name` attribute on `<activity>` points to the correct Activity class in its new location. If the old manifest relied on a default or short class name, you may need to use the fully qualified name:
```xml
<activity
android:name="com.example.app.MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
```
The shared module may still have a minimal manifest (auto-generated or containing just `<manifest>` with no `<application>`).
### 3. Application Class (if any)
```
composeApp/src/androidMain/kotlin/.../MyApplication.kt
--> androidApp/src/main/kotlin/.../MyApplication.kt
```
### 4. App-Level Resources
- Launcher icons (`mipmap-*`)
- App theme definitions that reference `applicationId`
- Splash screen resources
- Navigation graphs (if not shared)
```
composeApp/src/androidMain/res/mipmap-*/
composeApp/src/androidMain/res/values/themes.xml (app-level theme)
--> androidApp/src/main/res/
```
### 5. ProGuard Rules
```
composeApp/proguard-rules.pro
--> androidApp/proguard-rules.pro
```
---
## What Stays in shared
- All `commonMain` code (ViewModels, repositories, models, shared composables)
- All `expect`/`actual` declarations
- All `iosMain`, `desktopMain`, `wasmJsMain` code
- Framework export configuration (`binaries.framework`)
- Shared Android resources (strings, drawables used by shared composables)
- Shared Android-specific implementations (`actual` functions)
---
## Namespace Requirements
The `namespace` for each module must be unique:
```kotlin
// shared/build.gradle.kts
kotlin {
android {
namespace = "com.example.shared"
}
}
// androidApp/build.gradle.kts
android {
namespace = "com.example.app"
}
```
If they collide, you will get duplicate R class errors at compile time. The `applicationId` (in androidApp only) can be different from both namespaces.
---
## Run Configuration Updates
### Android Studio
After the split, the run configuration for the Android app must point to `:androidApp` instead of `:composeApp`:
1. Edit Run Configurations
2. Change Module to `androidApp`
3. Ensure the launch activity is `com.example.app.MainActivity`
### Xcode (iOS)
If the shared module was renamed from `composeApp` to `shared`:
1. **Update `baseName` in `shared/build.gradle.kts`** to match the new module name:
```kotlin
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "Shared" // was "ComposeApp"
isStatic = true
}
}
```
2. **Update the Run Script build phase** in `project.pbxproj` (or via Xcode > Build Phases > Run Script) to reference the new module:
```bash
# Old
cd "$SRCROOT/.."
./gradlew :composeApp:embedAndSignAppleFrameworkForXcode
# New
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode
```
3. **Update Swift imports** — in all `.swift` files, change the framework import to match `baseName`:
```swift
// Old
import ComposeApp
// New
import Shared
```
4. **Update the app struct name** if it was tied to the old module name. The `@main` struct name in your SwiftUI app entry point is independent of the framework name, but if it referenced the old name, rename it:
```swift
// Example: rename if it was called ComposeAppApp or similar
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
```
5. **Update framework search paths** in Xcode Build Settings if they reference the old module directory path.
---
## Quick Checklist
- [ ] Create `androidApp/` directory with `build.gradle.kts`
- [ ] Move `MainActivity` and `Application` class to `androidApp`
- [ ] Move `AndroidManifest.xml` (full manifest) to `androidApp`
- [ ] Move app-level resources (launcher icons, app theme) to `androidApp`
- [ ] Move ProGuard rules to `androidApp`
- [ ] Convert `composeApp` to `shared` with KMP library plugin
- [ ] Remove application-only config (`applicationId`, `versionCode`, `buildTypes`) from shared
- [ ] Add `implementation(project(":shared"))` to androidApp dependencies
- [ ] Update `settings.gradle.kts` includes
- [ ] Update root `build.gradle.kts` plugin declarations
- [ ] Ensure namespaces are different between modules
- [ ] Update Android Studio run configuration
- [ ] Update Xcode project if iOS target exists
- [ ] Run `./gradlew :androidApp:assembleDebug` and `./gradlew :shared:assemble` to verify
references/MIGRATION-FULL-RESTRUCTURE.md
# Full Restructure: Extracting All Platform Entry Points
This guide covers the complete extraction of platform-specific entry points from a monolithic `composeApp` module into dedicated per-platform application modules. This is the most thorough migration path and results in a clean architecture where `shared` contains only cross-platform code.
---
## Target Architecture
```
shared/ # KMP library (all shared code)
build.gradle.kts # kotlin.multiplatform + com.android.kotlin.multiplatform.library
src/
commonMain/kotlin/ # Shared business logic + UI
androidMain/kotlin/ # Android expect/actual implementations
iosMain/kotlin/ # iOS expect/actual implementations
androidApp/ # Android application entry point
build.gradle.kts # com.android.application
src/main/
desktopApp/ # Desktop (JVM) application entry point
build.gradle.kts # org.jetbrains.compose + application {}
src/main/kotlin/
webApp/ # Wasm/JS web application entry point
build.gradle.kts # kotlin.multiplatform + wasmJs target
src/wasmJsMain/kotlin/
iosApp/ # iOS application (Xcode project, usually already separate)
iosApp.xcodeproj/
```
---
## Desktop Extraction
### Create desktopApp/build.gradle.kts
```kotlin
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
dependencies {
implementation(project(":shared"))
implementation(compose.desktop.currentOs)
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
}
compose.desktop {
application {
mainClass = "com.example.app.MainKt"
nativeDistributions {
targetFormats(
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Dmg,
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Msi,
org.jetbrains.compose.desktop.application.dsl.TargetFormat.Deb
)
packageName = "com.example.app"
packageVersion = "1.0.0"
macOS {
iconFile.set(project.file("icons/icon.icns"))
}
windows {
iconFile.set(project.file("icons/icon.ico"))
}
linux {
iconFile.set(project.file("icons/icon.png"))
}
}
}
}
```
### Move Desktop Entry Point
```
composeApp/src/desktopMain/kotlin/com/example/app/main.kt
--> desktopApp/src/main/kotlin/com/example/app/main.kt
```
Update to call shared code:
```kotlin
// desktopApp/src/main/kotlin/com/example/app/main.kt
package com.example.app
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import com.example.shared.App
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "My App"
) {
App()
}
}
```
### Remove Desktop from shared
In `shared/build.gradle.kts`, remove the `jvm("desktop")` target entirely. The desktop target only needs to exist in `desktopApp`.
**Before (in composeApp):**
```kotlin
kotlin {
jvm("desktop")
// ...
sourceSets {
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
}
}
}
}
compose.desktop {
application {
mainClass = "com.example.app.MainKt"
nativeDistributions { ... }
}
}
```
**After (in shared):**
```kotlin
kotlin {
// jvm("desktop") -- REMOVED
// No desktop target in shared module
// No compose.desktop block
}
```
If you have shared JVM code that both Android and Desktop use, you have two options:
1. Keep a `jvm()` target in shared (without the `application {}` block) and use intermediate source sets.
2. Put all shared code in `commonMain` and rely on the JVM dependency from `desktopApp`.
---
## Web/WasmJS Extraction
### Create webApp/build.gradle.kts
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
wasmJs {
browser {
commonWebpackConfig {
outputFileName = "app.js"
}
}
binaries.executable()
}
sourceSets {
wasmJsMain.dependencies {
implementation(project(":shared"))
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
}
}
}
```
### Move Web Entry Point
```
composeApp/src/wasmJsMain/kotlin/com/example/app/main.kt
--> webApp/src/wasmJsMain/kotlin/com/example/app/main.kt
```
Update to call shared code:
```kotlin
// webApp/src/wasmJsMain/kotlin/com/example/app/main.kt
package com.example.app
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.window.CanvasBasedWindow
import com.example.shared.App
@OptIn(ExperimentalComposeUiApi::class)
fun main() {
CanvasBasedWindow(canvasElementId = "ComposeTarget") {
App()
}
}
```
### Move Web Resources
```
composeApp/src/wasmJsMain/resources/index.html
--> webApp/src/wasmJsMain/resources/index.html
```
Update `index.html` if the output JS filename changed.
### Remove WasmJS from shared
In `shared/build.gradle.kts`, remove the `wasmJs {}` target:
```kotlin
kotlin {
// wasmJs { ... } -- REMOVED
}
```
If you need shared Wasm-compatible code, keep `wasmJs()` in shared as a library target (no `binaries.executable()`, no `browser {}` config).
---
## iOS Handling
iOS is typically already a separate Xcode project. The main considerations during restructure:
### Framework Export Stays in shared
```kotlin
// shared/build.gradle.kts
kotlin {
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "Shared" // Update if renamed from "ComposeApp"
isStatic = true
}
}
}
```
### Update Xcode Project
If the module was renamed from `composeApp` to `shared`:
1. **Framework import:** Change `import ComposeApp` to `import Shared` in all `.swift` files (must match `baseName` in the framework config).
2. **Gradle task path:** Update the Run Script build phase in `project.pbxproj` (or via Xcode > Build Phases):
```bash
# In Xcode Build Phases > Run Script
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode
```
3. **App struct name:** If the SwiftUI `@main` struct was named after the old module (e.g., `ComposeAppApp`), rename it to something appropriate for your project.
4. **Framework search paths:** Update Build Settings if they reference the old module directory path.
5. **Cocoapods (if used):** Update the pod spec name:
```kotlin
// shared/build.gradle.kts
kotlin {
cocoapods {
name = "Shared"
summary = "Shared KMP module"
// ...
}
}
```
---
## Module Rename: composeApp to shared
### 1. Rename the Directory
```bash
mv composeApp shared
```
### 2. Update settings.gradle.kts
```kotlin
// Before
include(":composeApp")
// After
include(":shared")
include(":androidApp")
include(":desktopApp")
include(":webApp")
```
### 3. Update Cross-Module Dependencies
Search all `build.gradle.kts` files for references to `:composeApp`:
```kotlin
// Before
implementation(project(":composeApp"))
// After
implementation(project(":shared"))
```
### 4. Update .idea / Workspace Files
If using IntelliJ/Android Studio, the IDE may cache the old module name. Either:
- Delete `.idea/` and re-import
- Or manually update `.idea/modules.xml` and related files
---
## Variant: Native UI (sharedLogic + sharedUI Split)
For projects where each platform has its own native UI and only business logic is shared:
```
sharedLogic/ # Pure KMP library (no Compose)
build.gradle.kts # kotlin.multiplatform + com.android.kotlin.multiplatform.library
src/
commonMain/kotlin/ # ViewModels, repositories, models, networking
androidMain/kotlin/ # Android-specific implementations
iosMain/kotlin/ # iOS-specific implementations
sharedUI/ # Optional: Compose Multiplatform UI
build.gradle.kts # kotlin.multiplatform + com.android.kotlin.multiplatform.library + compose
src/
commonMain/kotlin/ # Shared composables
androidMain/kotlin/ # Android-specific composables
androidApp/ # Native Android app
build.gradle.kts
src/main/ # Android UI (Compose or XML), depends on sharedLogic (and optionally sharedUI)
iosApp/ # Native iOS app (SwiftUI/UIKit)
# Depends on sharedLogic framework
```
### sharedLogic/build.gradle.kts
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKmpLibrary)
}
kotlin {
android {
namespace = "com.example.shared.logic"
compileSdk = 35
minSdk = 24
}
iosX64()
iosArm64()
iosSimulatorArm64()
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "SharedLogic"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.ktor.client.core)
implementation(libs.kotlinx.serialization.json)
}
}
}
```
This variant is useful when:
- iOS uses SwiftUI and does not want Compose Multiplatform
- Desktop is not a target
- You want to minimize the shared surface area
---
## Variant: Server (Backend Module)
For projects that include a Ktor/Spring server:
```
shared/ # KMP library (shared models, API contracts)
androidApp/
iosApp/
server/ # JVM server application
build.gradle.kts # kotlin("jvm") + ktor/spring plugin
src/main/kotlin/
```
### server/build.gradle.kts
```kotlin
plugins {
alias(libs.plugins.kotlinJvm)
alias(libs.plugins.ktor) // or spring boot
application
}
application {
mainClass.set("com.example.server.ApplicationKt")
}
dependencies {
implementation(project(":shared"))
implementation(libs.ktor.server.core)
implementation(libs.ktor.server.netty)
implementation(libs.logback.classic)
}
```
The server module is a plain JVM module. It depends on `:shared` for common models and API contracts. It is unaffected by the AGP 9.0 migration except that:
- If shared previously had a `jvm()` target that the server depended on, verify it still exists after restructuring.
- If shared was renamed, update the dependency path.
---
## settings.gradle.kts -- Final State
```kotlin
rootProject.name = "MyProject"
pluginManagement {
repositories {
google {
content {
includeGroupByRegex("com\\.android.*")
includeGroupByRegex("com\\.google.*")
includeGroupByRegex("androidx.*")
}
}
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
include(":shared")
include(":androidApp")
include(":desktopApp")
include(":webApp")
// include(":server") // if applicable
```
---
## Quick Checklist
- [ ] Create `androidApp/` with pure Android application plugin (see MIGRATION-APP-SPLIT.md)
- [ ] Create `desktopApp/` with compose desktop plugin and `application {}` block
- [ ] Create `webApp/` with wasmJs target and `binaries.executable()`
- [ ] Move `main()` functions from `composeApp/src/{platform}Main/` to respective app modules
- [ ] Move `compose.desktop.application {}` config to `desktopApp`
- [ ] Move `wasmJs { browser {} }` config to `webApp`
- [ ] Rename `composeApp` to `shared`
- [ ] Convert shared to KMP library plugin (`com.android.kotlin.multiplatform.library`)
- [ ] Remove platform app targets from shared (keep only library targets)
- [ ] Update all `settings.gradle.kts` includes
- [ ] Update all `project(":composeApp")` references to `project(":shared")`
- [ ] Update Xcode project (framework name, Gradle task path, Swift imports)
- [ ] Verify each app module builds independently
- [ ] Run all platform targets to confirm functionality
references/MIGRATION-LIBRARY.md
# Migrating a KMP Library Module to AGP 9.0
This reference covers the full migration of a Kotlin Multiplatform library module from `com.android.library` (AGP 8.x) to `com.android.kotlin.multiplatform.library` (AGP 9.x).
---
## build.gradle.kts -- Before (AGP 8.x)
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions { jvmTarget = "11" }
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
}
androidMain.dependencies {
implementation(libs.androidx.appcompat)
}
}
}
android {
namespace = "com.example.shared"
compileSdk = 34
defaultConfig { minSdk = 24 }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
debugImplementation(libs.compose.ui.tooling)
}
```
## build.gradle.kts -- After (AGP 9.x)
```kotlin
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKmpLibrary)
}
kotlin {
android {
namespace = "com.example.shared"
compileSdk = 35
minSdk = 24
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
androidResources { enable = true }
}
iosX64()
iosArm64()
iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
}
androidMain.dependencies {
implementation(libs.androidx.appcompat)
}
}
}
dependencies {
androidRuntimeClasspath(libs.compose.ui.tooling)
}
```
---
## Version and Plugin Changes
### With Version Catalog (`gradle/libs.versions.toml`)
**Before:**
```toml
[versions]
agp = "8.7.3"
kotlin = "2.1.0"
[plugins]
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
```
**After:**
```toml
[versions]
agp = "9.0.1"
kotlin = "2.3.20"
[plugins]
androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
```
### Without Version Catalog
If versions are declared directly in build files, update the plugin IDs and versions in place:
**Before (root build.gradle.kts):**
```kotlin
plugins {
id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.1.0" apply false
}
```
**After (root build.gradle.kts):**
```kotlin
plugins {
id("com.android.kotlin.multiplatform.library") version "9.0.1" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.3.20" apply false
}
```
**Before (module build.gradle.kts):**
```kotlin
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
}
```
**After (module build.gradle.kts):**
```kotlin
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.kotlin.multiplatform.library")
}
```
Key changes:
- The plugin ID changes from `com.android.library` to `com.android.kotlin.multiplatform.library`.
- AGP version must be 9.0.0+, Gradle 9.1.0+, KGP 2.0.0+ (2.3.0+ recommended).
---
## Root build.gradle.kts Changes
### With Version Catalog
**Before:**
```kotlin
plugins {
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}
```
**After:**
```kotlin
plugins {
alias(libs.plugins.androidKmpLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}
```
### Without Version Catalog
**Before:**
```kotlin
plugins {
id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.1.0" apply false
}
```
**After:**
```kotlin
plugins {
id("com.android.kotlin.multiplatform.library") version "9.0.1" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.3.20" apply false
}
```
No other root-level changes are required unless you have convention plugins that reference the old plugin ID (see convention plugin section below).
---
## Source Directory Renames
The new KMP-integrated plugin does NOT change the expected source directory layout. The standard KMP source sets still apply:
| Source Set | Directory |
|-------------------------|---------------------------|
| `commonMain` | `src/commonMain/kotlin/` |
| `androidMain` | `src/androidMain/kotlin/` |
| `androidMain` resources | `src/androidMain/res/` |
| `iosMain` | `src/iosMain/kotlin/` |
**No renames are required** if you already use the standard KMP layout. If your module previously used the classic Android layout (`src/main/java/`, `src/main/res/`), you must migrate to the KMP layout:
| Old (Android layout) | New (KMP layout) |
|--------------------------------|---------------------------------------|
| `src/main/java/` | `src/androidMain/kotlin/` |
| `src/main/res/` | `src/androidMain/res/` |
| `src/main/AndroidManifest.xml` | `src/androidMain/AndroidManifest.xml` |
| `src/test/java/` | `src/androidHostTest/kotlin/` |
| `src/androidTest/java/` | `src/androidDeviceTest/kotlin/` |
---
## Test Configuration
The new plugin uses explicit opt-in for test source sets.
### Host Tests (Unit Tests)
```kotlin
kotlin {
android {
// Enable unit tests (JVM-based, run on host machine)
withHostTest {
// Optional: configure the host test compilation
}
}
}
```
This creates the `androidHostTest` source set. The previous name `androidUnitTest` still works as an alias but `androidHostTest` is preferred.
### Device Tests (Instrumented Tests)
```kotlin
kotlin {
android {
// Enable instrumented tests (run on device/emulator)
withDeviceTest {
// Optional: configure the device test compilation
}
}
}
```
This creates the `androidDeviceTest` source set. The previous name `androidInstrumentedTest` still works as an alias but `androidDeviceTest` is preferred.
### Full Test Example
```kotlin
kotlin {
android {
namespace = "com.example.shared"
compileSdk = 35
minSdk = 24
withHostTest {}
withDeviceTest {}
}
sourceSets {
getByName("androidHostTest").dependencies {
implementation(libs.junit)
implementation(libs.robolectric)
}
getByName("androidDeviceTest").dependencies {
implementation(libs.androidx.test.runner)
implementation(libs.androidx.test.espresso.core)
}
}
}
```
---
## Java Compilation (withJava)
If your module contains Java source files in `androidMain`, you must explicitly enable Java compilation:
```kotlin
kotlin {
android {
withJava()
}
}
```
Without this call, `.java` files in `src/androidMain/java/` will be ignored. Kotlin files are compiled by default.
---
## Consumer ProGuard Rules
### Before (AGP 8.x)
```kotlin
android {
defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}
}
```
### After (AGP 9.x)
```kotlin
kotlin {
android {
consumerProguardFiles.add(file("consumer-rules.pro"))
}
}
```
**Warning:** Consumer ProGuard rules can be silently dropped during migration if you forget this step. The old `android {}` block is gone, so the `consumerProguardFiles` call in `defaultConfig` has no equivalent location unless you explicitly add it in `kotlin { android {} }`.
---
## JVM Target Configuration Hierarchy
There are three levels at which you can configure the JVM target. They are listed from most specific (highest priority) to least specific (lowest priority):
### Level 1: Android-Specific Compiler Options (Recommended)
```kotlin
kotlin {
android {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
}
```
This sets the JVM target only for the Android compilation.
### Level 2: Top-Level Kotlin Compiler Options
```kotlin
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}
```
This sets the JVM target for ALL JVM-based compilations in the project (Android, JVM desktop, etc.).
### Level 3: Gradle Toolchain
```kotlin
kotlin {
jvmToolchain(11)
}
```
This sets both the JDK used for compilation and the JVM target. It is the broadest setting and affects all JVM compilations.
### Priority Order
If multiple levels are set, the most specific wins:
1. `kotlin { android { compilerOptions { } } }` -- highest priority
2. `kotlin { compilerOptions { } }` -- medium priority
3. `kotlin { jvmToolchain() }` -- lowest priority
### Migration from kotlinOptions
The old `kotlinOptions` DSL is removed:
```kotlin
// REMOVED in AGP 9.0 -- do not use
androidTarget {
compilations.all {
kotlinOptions { jvmTarget = "11" }
}
}
```
Replace with one of the three levels above.
---
## Dependencies Configuration Changes
The top-level `dependencies {}` block configurations change because build variants (debug/release) are removed from the KMP library plugin.
### Before
```kotlin
dependencies {
debugImplementation(libs.compose.ui.tooling)
releaseImplementation(libs.some.lib)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.runner)
}
```
### After
```kotlin
dependencies {
// Use string-based configuration names
"androidRuntimeClasspath"(libs.compose.ui.tooling)
// Or use sourceSets for most dependencies
}
kotlin {
sourceSets {
androidMain.dependencies {
implementation(libs.some.lib)
}
getByName("androidHostTest").dependencies {
implementation(libs.junit)
}
getByName("androidDeviceTest").dependencies {
implementation(libs.androidx.test.runner)
}
}
}
```
**Prefer putting dependencies inside `sourceSets` blocks** rather than the top-level `dependencies {}` block. The top-level block is only needed for special configurations like `androidRuntimeClasspath` that have no source set equivalent.
---
## Dependency Resolution Details
When your KMP module depends on a legacy Android library that exposes multiple variants (e.g., `debug`/`release` build types or custom flavor dimensions like `free`/`paid`), you must explicitly define how to resolve them using the `localDependencySelection` DSL.
### Before
```kotlin
android {
defaultConfig {
// The consuming module doesn't have a 'tier' dimension,
// so it tells Gradle to use the 'free' flavor of dependencies
missingDimensionStrategy("tier", "free")
}
buildTypes {
getByName("debug") {
// If the dependency doesn't have a 'debug' build type, fallback to 'release'
matchingFallbacks.add("release")
}
}
}
```
### After
```kotlin
kotlin {
android {
localDependencySelection {
// Determine which build type to consume from Android library dependencies, in order of preference
selectBuildTypeFrom.set(listOf("debug", "release"))
// Map the missing custom flavor dimensions directly
productFlavorDimension("tier") {
selectFrom.set(listOf("free"))
}
}
}
}
```
---
## Android Resources
Android resources (`res/`) are not processed by default with the new plugin. You must explicitly enable them:
```kotlin
kotlin {
android {
androidResources { enable = true }
}
}
```
Without this, files in `src/androidMain/res/` will be ignored and `R` class generation will not happen.
---
## Convention Plugin Refactoring
If you use convention plugins (build-logic), update them:
### Before
```kotlin
// build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
class KmpLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("com.android.library")
pluginManager.apply("org.jetbrains.kotlin.multiplatform")
extensions.configure<LibraryExtension> {
compileSdk = 34
defaultConfig.minSdk = 24
}
}
}
}
```
### After
```kotlin
// build-logic/convention/src/main/kotlin/KmpLibraryConventionPlugin.kt
class KmpLibraryConventionPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("org.jetbrains.kotlin.multiplatform")
pluginManager.apply("com.android.kotlin.multiplatform.library")
extensions.configure<KotlinMultiplatformExtension> {
android {
namespace = // set per-module or pass as parameter
compileSdk = 35
minSdk = 24
}
}
}
}
}
```
The `LibraryExtension` class from AGP is no longer used. All Android configuration goes through `KotlinMultiplatformExtension.android {}`.
---
## Quick Checklist
- [ ] Update plugin IDs and versions (in `libs.versions.toml` if using version catalog, or directly in build files)
- [ ] Replace plugin alias in `build.gradle.kts`
- [ ] Move `android {}` block contents into `kotlin { android {} }`
- [ ] Replace `androidTarget {}` with `android {}`
- [ ] Replace `kotlinOptions` with `compilerOptions`
- [ ] Enable `androidResources` if using Android resources
- [ ] Enable `withHostTest {}` if there are any android host tests or common tests
- [ ] Enable `withDeviceTest {}` if there are any android device tests
- [ ] Add `withJava()` if module contains Java source files
- [ ] Move consumer ProGuard rules to new DSL
- [ ] Migrate top-level `dependencies` to source set dependencies
- [ ] Update convention plugins if applicable
- [ ] Rename test source dirs: `androidUnitTest` to `androidHostTest`, `androidInstrumentedTest` to `androidDeviceTest`
- [ ] Update root `build.gradle.kts` plugin declarations
- [ ] Run `./gradlew :module:assemble` to verify
- [ ] Run `./gradlew :module:testAndroidHostTest` if there are any android host tests or common tests
- [ ] Run `./gradlew :module:assembleAndroidDeviceTest` if there are any android device tests
references/PLUGIN-COMPATIBILITY.md
# Plugin Compatibility: AGP 9.0
AGP 9.0 introduces breaking changes that affect many third-party plugins. **Before migrating, check
which plugins the project uses and whether they are compatible.**
---
## Known Compatible Plugins (minimum version required)
| Plugin | Minimum Compatible Version | Notes |
|-------------------------------------|----------------------------|--------------------------------------------------------------------------------------------------------------------------------------|
| `com.google.devtools.ksp` | 2.3.1 (2.3.3+ recommended) | 2.3.1 adds AGP 9.0 support; 2.3.3+ fixes deprecated compilerOptions KGP API usage. May need `android.disallowKotlinSourceSets=false` |
| `com.google.dagger.hilt.android` | 2.59 | — |
| `com.google.firebase.firebase-perf` | 2.0.2 | — |
| `androidx.navigation.safeargs` | 2.9.5 | — |
| `org.jetbrains.compose` | 1.9.3 | — |
| `org.jetbrains.dokka` | 2.2.0-Beta | — |
| `app.cash.burst` | 2.10.0 | — |
| `com.google.firebase.testlab` | 0.0.1-alpha11 | — |
---
## Plugins Requiring Opt-Out Flags
These work but require temporarily setting `android.newDsl=false` (or other flags):
| Plugin | Workaround |
|---------------------------------------------------------|-----------------------------------------------------------------------------------------------------|
| `androidx.baselineprofile` (< 1.5.0-alpha01) | `android.newDsl=false` |
| `de.mannodermaus.android-junit5` (< 1.13.4.0) | `android.newDsl=false` |
| `com.google.android.gms:oss-licenses-plugin` (< 0.10.8) | `android.newDsl=false` |
| `com.apollographql.apollo` (< 4.4.0) | `android.newDsl=false` |
| `org.gradle.android.cache-fix` (< 3.0.2) | `android.newDsl=false` |
| `com.github.triplet.play` (< 4.0.0) | `android.newDsl=false` |
| `app.cash.sqldelight` | `android.newDsl=false` + `android.disallowKotlinSourceSets=false` |
| `com.google.protobuf` | `android.newDsl=false` |
| `app.cash.paparazzi` | `android.newDsl=false` |
| `io.gitlab.arturbosch.detekt` (< 2.0.0) | `android.newDsl=false` + `android.builtInKotlin=false` |
| `org.jlleitschuh.gradle.ktlint` | `android.builtInKotlin=false` |
| `dev.icerock.mobile.multiplatform-resources` (< 0.26.0) | `android.builtInKotlin=false` + `android.newDsl=false` + `android.sourceset.disallowProvider=false` |
---
## Known Broken Plugins (No Workaround)
| Plugin | Status |
|------------------------------|---------------------------|
| `com.newrelic.agent.android` | Incompatible with AGP 9.0 |
| `com.huawei.agconnect.agcp` | Incompatible with AGP 9.0 |
---
## What To Do
1. **Inventory all plugins** used in the project
2. **Check each against the tables above**
3. **If any plugin is broken without workaround**, inform the user — they may need to wait for a plugin update or remove it
4. **If plugins need opt-out flags**, add them to `gradle.properties` and note them as temporary workarounds
5. **Update plugin versions** to their AGP 9.0-compatible versions before or during migration
references/VERSION-MATRIX.md
# Version Compatibility Matrix for KMP AGP 9.0 Migration
---
## Compatibility Table
| Component | Minimum | Recommended | Notes |
|-------------------------------|--------------------|---------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| AGP | 9.0.0 | 9.0.1+ | 9.0.0 is the initial release; 9.0.1+ includes early bug fixes |
| Gradle | 9.1.0 | 9.1.0+ | AGP 9.0 requires Gradle 9.1+; earlier Gradle versions will not work |
| JDK | 17 | 17+ | AGP 9.0 requires JDK 17 minimum |
| SDK Build Tools | 36.0.0 | 36.0.0 | Required by AGP 9.0 |
| KGP (Kotlin Gradle Plugin) | 2.0.0 | 2.3.0+ | 2.0.0 is minimum for KMP library plugin; 2.3.0+ has best compatibility |
| KGP (built-in Kotlin runtime) | 2.2.10 | 2.3.0+ | AGP 9.0 has runtime dependency on KGP 2.2.10; auto-upgrades if lower |
| KSP | 2.3.1 | 2.3.6 | KSP version is no longer tied to the Kotlin compiler version since 2.3.0. AGP 9.0 and built-in Kotlin support added in 2.3.1. KSP migrated away from the deprecated compilerOptions KGP API in 2.3.3; earlier versions may have compatibility problems with other Gradle plugins |
| NDK | — | 28.2.13676358 | Default changed to r28c; specify explicitly if needed |
| Android Studio | Otter 3 (2025.2.3) | Latest stable | First version with full AGP 9.0 + KMP library plugin IDE support |
| IntelliJ IDEA | Not supported | — | Does not support AGP 9.0 as of 2026.1, use Android Studio instead. Can still be used for non-Android KMP targets (JVM, iOS, JS/Wasm) |
| Max API Level | — | 36.1 | Highest supported API level in AGP 9.0 |
| Compose Multiplatform | 1.9.3 | 1.10.0+ | AGP 9.0 support was added in 1.9.3 |
| Compose Compiler Plugin | 2.0.0 | Matches KGP version | Since KGP 2.0, use `org.jetbrains.kotlin.plugin.compose` — version is tied to KGP automatically |
| Kotlin Coroutines | 1.8.0 | 1.10.0+ | 1.8.0+ for full K2 support |
| Kotlin Serialization | 1.6.0 | 1.8.0+ | 1.8.0+ for K2 compiler plugin support |
| Ktor | 2.3.0 | 3.0.0+ | 3.0.0 for best KMP library plugin compatibility |
| Room (KMP) | 2.7.0 | 2.8.0+ | KMP Room requires KSP; verify KSP compatibility |
---
## Version Notes
### AGP 9.0.0
- First release supporting `com.android.kotlin.multiplatform.library`.
- Built-in Kotlin compilation for `com.android.application` and `com.android.library` (no separate `kotlin-android` plugin needed).
- Removes support for `com.android.application` + `org.jetbrains.kotlin.multiplatform` in the same module.
- Single-variant model for KMP libraries (no build types/flavors).
- Runtime dependency on KGP 2.2.10 — projects using lower KGP versions are auto-upgraded.
- If the project uses KSP, upgrade to 2.3.1+ for AGP 9.0 support.
- New DSL interfaces only — `BaseExtension` and legacy types removed.
- `org.jetbrains.kotlin.kapt` incompatible — use KSP or `com.android.legacy-kapt`.
- Java source/target default changed from Java 8 to Java 11.
- R class is compile-time non-final in application modules by default.
- `targetSdk` defaults to `compileSdk` when not set (was `minSdk`).
- NDK default changed to r28c.
- Requires JDK 17+, Gradle 9.1.0+, SDK Build Tools 36.0.0.
- Many Gradle property defaults changed — see SKILL.md "Gradle Properties Default Changes".
- Removed: embedded Wear OS app support, density split APKs, legacy variant APIs.
- New: IDE support for test fixtures, fused library plugin (preview).
### AGP 9.0.1+
- Bug fixes for KMP library plugin edge cases.
- Improved error messages for common migration mistakes.
- Better IDE sync performance.
### KGP 2.3.0+
- Best compatibility with KMP AGP 9.0 library plugin.
- Improved multiplatform source set inference.
- Better error diagnostics for KMP configuration issues.
- Stable Compose compiler plugin integration.
---
## Upgrade Path
### From AGP 8.x + KGP 1.9.x
1. Upgrade KGP to 2.0.0+ first (can be done on AGP 8.x).
2. Migrate `kotlinOptions` to `compilerOptions`.
3. Upgrade Gradle to 9.1.0.
4. Upgrade AGP to 9.0.1+.
5. Migrate library plugins to `com.android.kotlin.multiplatform.library`.
6. Upgrade KGP to 2.3.0+ for best experience.
### From AGP 8.x + KGP 2.0.x
1. Upgrade Gradle to 9.1.0.
2. Upgrade AGP to 9.0.1+.
3. Migrate library plugins to `com.android.kotlin.multiplatform.library`.
4. Upgrade KGP to 2.3.0+ for best experience.
---
## gradle/wrapper/gradle-wrapper.properties
```properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1-bin.zip
```
---
## Basic libs.versions.toml template
```toml
[versions]
agp = "9.0.1"
kotlin = "2.3.20"
compose-multiplatform = "1.10.3"
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinJvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
```
---
## Compatibility Validation Commands
Run these to verify your setup is compatible:
```bash
# Check Gradle version
./gradlew --version
# Check AGP version applied
./gradlew buildEnvironment | grep -e "com.android.library" -e "com.android.application" -e "com.android.kotlin.multiplatform.library"
# Check KGP version
./gradlew buildEnvironment | grep "org.jetbrains.kotlin:kotlin-gradle-plugin"
# Verify the KMP library plugin is recognized
./gradlew :shared:tasks --group=build
# Full validation build
./gradlew :shared:assemble :androidApp:assembleDebug
```
scripts/analyze-project.sh
#!/bin/sh
#
# analyze-project.sh - Analyze a Gradle/KMP project for AGP 9.0 migration readiness
#
# Usage: ./analyze-project.sh [PROJECT_ROOT]
# Defaults to current directory if PROJECT_ROOT is not specified.
set -e
PROJECT_ROOT="${1:-.}"
# Resolve to absolute path
PROJECT_ROOT="$(cd "$PROJECT_ROOT" && pwd)"
echo "========================================"
echo " KMP AGP 9.0 Migration - Project Analysis"
echo "========================================"
echo ""
echo "Project root: $PROJECT_ROOT"
echo ""
# --- Gradle Version ---
echo "----------------------------------------"
echo " Gradle Version"
echo "----------------------------------------"
WRAPPER_PROPS="$PROJECT_ROOT/gradle/wrapper/gradle-wrapper.properties"
if [ -f "$WRAPPER_PROPS" ]; then
GRADLE_URL=$(grep 'distributionUrl' "$WRAPPER_PROPS" | sed 's/.*=//' | sed 's/\\//g')
GRADLE_VERSION=$(echo "$GRADLE_URL" | sed 's|.*gradle-||' | sed 's|-.*||')
echo " Distribution URL: $GRADLE_URL"
echo " Gradle version: $GRADLE_VERSION"
else
echo " WARNING: gradle-wrapper.properties not found"
GRADLE_VERSION="unknown"
fi
echo ""
# --- AGP Version ---
echo "----------------------------------------"
echo " Android Gradle Plugin Version"
echo "----------------------------------------"
TOML_FILE="$PROJECT_ROOT/gradle/libs.versions.toml"
if [ -f "$TOML_FILE" ]; then
AGP_VERSION=$(grep '^agp' "$TOML_FILE" | head -1 | sed 's/.*= *"//' | sed 's/".*//')
if [ -n "$AGP_VERSION" ]; then
echo " AGP version (from version catalog): $AGP_VERSION"
else
echo " AGP version not found in version catalog"
AGP_VERSION="unknown"
fi
else
echo " WARNING: libs.versions.toml not found"
AGP_VERSION="unknown"
fi
KOTLIN_VERSION=$(grep '^kotlin' "$TOML_FILE" 2>/dev/null | head -1 | sed 's/.*= *"//' | sed 's/".*//')
if [ -n "$KOTLIN_VERSION" ]; then
echo " Kotlin version: $KOTLIN_VERSION"
fi
echo ""
# --- Module Analysis ---
echo "----------------------------------------"
echo " Module Analysis"
echo "----------------------------------------"
# Find all build.gradle.kts and build.gradle files
BUILD_FILES=$(find "$PROJECT_ROOT" -name "build.gradle.kts" -o -name "build.gradle" | grep -v '.gradle/' | grep -v 'build/' | sort)
for BUILD_FILE in $BUILD_FILES; do
REL_PATH=$(echo "$BUILD_FILE" | sed "s|$PROJECT_ROOT/||")
MODULE_DIR=$(dirname "$BUILD_FILE")
REL_MODULE=$(echo "$MODULE_DIR" | sed "s|$PROJECT_ROOT||" | sed 's|^/||')
if [ -z "$REL_MODULE" ]; then
MODULE_NAME="(root)"
else
MODULE_NAME=":$(echo "$REL_MODULE" | sed 's|/|:|g')"
fi
echo ""
echo " Module: $MODULE_NAME"
echo " File: $REL_PATH"
# Detect plugins
HAS_ANDROID_APP="no"
HAS_ANDROID_LIB="no"
HAS_KMP="no"
HAS_KOTLIN_ANDROID="no"
HAS_COMPOSE="no"
HAS_APPLY_FALSE="no"
if grep -q 'com.android.application\|androidApplication' "$BUILD_FILE"; then
if grep -q 'apply false' "$BUILD_FILE" 2>/dev/null; then
HAS_APPLY_FALSE="yes"
else
HAS_ANDROID_APP="yes"
fi
fi
if grep -q 'com.android.library\|androidLibrary' "$BUILD_FILE"; then
if grep -q 'apply false' "$BUILD_FILE" 2>/dev/null; then
HAS_APPLY_FALSE="yes"
else
HAS_ANDROID_LIB="yes"
fi
fi
if grep -q 'kotlin.multiplatform\|kotlin("multiplatform")\|kotlinMultiplatform' "$BUILD_FILE"; then
HAS_KMP="yes"
fi
if grep -q 'kotlin.android\|kotlin("android")\|kotlinAndroid' "$BUILD_FILE"; then
HAS_KOTLIN_ANDROID="yes"
fi
if grep -q 'org.jetbrains.compose\|composeMultiplatform' "$BUILD_FILE"; then
HAS_COMPOSE="yes"
fi
echo " Plugins detected:"
[ "$HAS_ANDROID_APP" = "yes" ] && echo " - com.android.application"
[ "$HAS_ANDROID_LIB" = "yes" ] && echo " - com.android.library"
[ "$HAS_KMP" = "yes" ] && echo " - kotlin.multiplatform"
[ "$HAS_KOTLIN_ANDROID" = "yes" ] && echo " - kotlin.android"
[ "$HAS_COMPOSE" = "yes" ] && echo " - org.jetbrains.compose"
[ "$HAS_APPLY_FALSE" = "yes" ] && echo " - (declarations with apply false — root buildscript)"
# Check for android {} block
HAS_ANDROID_BLOCK="no"
if grep -q '^android {' "$BUILD_FILE" || grep -q '^android {' "$BUILD_FILE"; then
HAS_ANDROID_BLOCK="yes"
echo " Has android {} block: yes"
fi
# Check source set layout
if [ -d "$MODULE_DIR/src/main" ]; then
echo " Source layout: src/main (legacy Android)"
fi
if [ -d "$MODULE_DIR/src/androidMain" ]; then
echo " Source layout: src/androidMain (KMP)"
fi
if [ -d "$MODULE_DIR/src/commonMain" ]; then
echo " Source layout: src/commonMain (KMP)"
fi
# Determine migration recommendation
echo " Migration recommendation:"
if [ "$HAS_APPLY_FALSE" = "yes" ]; then
echo " -> Root buildscript: update plugin versions only"
elif [ "$HAS_KMP" = "yes" ] && [ "$HAS_ANDROID_LIB" = "yes" ]; then
echo " -> Replace com.android.library with android-kotlin-multiplatform-library"
echo " -> Move android {} config into androidTarget {} in kotlin {} block"
echo " -> Remove the standalone android {} block"
elif [ "$HAS_KMP" = "yes" ] && [ "$HAS_ANDROID_APP" = "yes" ]; then
echo " -> Split into separate androidApp module (com.android.application)"
echo " -> Convert shared KMP module to use android-kotlin-multiplatform-library"
echo " -> Move Android entry point (Activity) to the new androidApp module"
elif [ "$HAS_ANDROID_APP" = "yes" ] && [ "$HAS_KMP" = "no" ]; then
echo " -> Pure Android app module: update AGP to 9.x, no KMP migration needed"
elif [ "$HAS_ANDROID_LIB" = "yes" ] && [ "$HAS_KMP" = "no" ]; then
echo " -> Pure Android library: update AGP to 9.x, no KMP migration needed"
echo " -> (Consider converting to KMP if cross-platform is desired)"
elif [ "$HAS_KOTLIN_ANDROID" = "yes" ]; then
echo " -> Replace org.jetbrains.kotlin.android with kotlin.multiplatform if going KMP"
echo " -> Or keep as-is and just update AGP version"
else
echo " -> No Android plugins detected: no AGP migration needed"
fi
done
echo ""
# --- Gradle Properties Check ---
echo "----------------------------------------"
echo " Gradle Properties"
echo "----------------------------------------"
GRADLE_PROPS="$PROJECT_ROOT/gradle.properties"
if [ -f "$GRADLE_PROPS" ]; then
LEGACY_FLAGS=""
if grep -q 'android.enableLegacyVariantApi' "$GRADLE_PROPS"; then
LEGACY_FLAGS="$LEGACY_FLAGS\n - android.enableLegacyVariantApi (must be removed for AGP 9.0)"
fi
if grep -q 'android.useAndroidX' "$GRADLE_PROPS"; then
LEGACY_FLAGS="$LEGACY_FLAGS\n - android.useAndroidX (default in AGP 9.0, can be removed)"
fi
if grep -q 'android.enableJetifier' "$GRADLE_PROPS"; then
LEGACY_FLAGS="$LEGACY_FLAGS\n - android.enableJetifier (removed in AGP 9.0, must be removed)"
fi
if grep -q 'android.nonTransitiveRClass' "$GRADLE_PROPS"; then
LEGACY_FLAGS="$LEGACY_FLAGS\n - android.nonTransitiveRClass (default in AGP 9.0, can be removed)"
fi
if [ -n "$LEGACY_FLAGS" ]; then
echo " Legacy flags found:"
printf "$LEGACY_FLAGS\n"
else
echo " No legacy flags found"
fi
else
echo " No gradle.properties file found"
fi
echo ""
# --- Summary ---
echo "========================================"
echo " Summary"
echo "========================================"
echo ""
echo " Current AGP version: $AGP_VERSION"
echo " Current Gradle version: $GRADLE_VERSION"
echo " Target AGP version: 9.0.0+"
echo " Target Gradle version: 9.1.0+"
echo ""
if [ "$AGP_VERSION" != "unknown" ]; then
AGP_MAJOR=$(echo "$AGP_VERSION" | cut -d. -f1)
if [ "$AGP_MAJOR" -ge 9 ] 2>/dev/null; then
echo " Status: Project appears to already be on AGP 9.0+"
else
echo " Status: Project needs migration from AGP $AGP_VERSION to 9.0+"
fi
fi
echo ""
echo "========================================"
echo " Run the migration skill for guided assistance."
echo "========================================"
SKILL.md
---
name: kotlin-tooling-agp9-migration
description: >
Migrates Kotlin Multiplatform (KMP) projects to Android Gradle Plugin 9.0+.
Handles plugin replacement (com.android.kotlin.multiplatform.library), module
splitting, DSL migration, and the new default project structure. Use when
upgrading AGP, when build fails due to KMP+AGP incompatibility, or when the
user mentions AGP 9.0, android multiplatform plugin, KMP migration, or
com.android.kotlin.multiplatform.library.
license: Apache-2.0
metadata:
author: JetBrains
version: "1.0.0"
---
# KMP AGP 9.0 Migration
Android Gradle Plugin 9.0 makes the Android application and library plugins incompatible
with the Kotlin Multiplatform plugin in the same module. This skill guides you through the
migration.
## Step 0: Analyze the Project
Before making any changes, understand the project structure:
1. Read `settings.gradle.kts` (or `.gradle`) to find all modules
2. For each module, read its `build.gradle.kts` to identify which plugins are applied
3. Check if the project uses a Gradle version catalog (`gradle/libs.versions.toml`). If it exists,
read it for current AGP/Gradle/Kotlin versions. If not, find versions directly in `build.gradle.kts`
files (typically in the root `buildscript {}` or `plugins {}` block). **Adapt all examples in this
guide accordingly** — version catalog examples use `alias(libs.plugins.xxx)` while direct usage
uses `id("plugin.id") version "x.y.z"`
4. Read `gradle/wrapper/gradle-wrapper.properties` for the Gradle version
5. Check `gradle.properties` for any existing workarounds (`android.enableLegacyVariantApi`)
6. Check for `org.jetbrains.kotlin.android` plugin usage — AGP 9.0 has built-in Kotlin and this plugin must be removed
7. Check for `org.jetbrains.kotlin.kapt` plugin usage — incompatible with built-in Kotlin, must migrate to KSP or `com.android.legacy-kapt`
8. Check for third-party plugins that may be incompatible with AGP 9.0 (see "Plugin Compatibility" section below)
If Bash is available, run `scripts/analyze-project.sh` from this skill's directory to get a structured summary.
### Classify Each Module
For each module, determine its type:
| Current plugins | Migration path |
|--------------------------------------------------------------------------|---------------------------------------------|
| `kotlin.multiplatform` + `com.android.library` | **Path A** — Library plugin swap |
| `kotlin.multiplatform` + `com.android.application` | **Path B** — Mandatory Android split |
| `kotlin.multiplatform` with multiple platform entry points in one module | **Path C** — Full restructure (recommended) |
| `com.android.application` or `com.android.library` (no KMP) | See "Pure Android Tips" below |
### Determine Scope
- **Path B is mandatory** for any module combining KMP + Android application plugin
- **Path C is recommended** when the project has a monolithic `composeApp` (or similar) module
containing entry points for multiple platforms (Android, Desktop, Web). This aligns with the
new JetBrains default project structure where each platform gets its own app module.
- **Ask the user** whether they want Path B only (minimum required) or Path C (recommended full restructure)
## Path A: Library Module Migration
Use this when a module applies `kotlin.multiplatform` + `com.android.library`.
See [references/MIGRATION-LIBRARY.md](references/MIGRATION-LIBRARY.md) for full before/after code.
Summary:
1. **Replace plugin**: `com.android.library` → `com.android.kotlin.multiplatform.library`
2. **Remove `org.jetbrains.kotlin.android`** plugin if present (AGP 9.0 has built-in Kotlin support)
3. **Migrate DSL**: Move config from top-level `android {}` block into `kotlin { android {} }`:
```kotlin
kotlin {
android {
namespace = "com.example.lib"
compileSdk = 35
minSdk = 24
}
}
```
4. **Rename source directories** (only if the module uses classic Android layout instead of KMP layout):
- `src/main` → `src/androidMain`
- `src/test` → `src/androidHostTest`
- `src/androidTest` → `src/androidDeviceTest`
- If the module already uses `src/androidMain/`, no directory renames are needed
5. **Move dependencies** from top-level `dependencies {}` into `sourceSets`:
```kotlin
kotlin {
sourceSets {
androidMain.dependencies {
implementation("androidx.appcompat:appcompat:1.7.0")
}
}
}
```
6. **Enable resources** explicitly if the module uses Android or Compose Multiplatform resources:
```kotlin
kotlin {
android {
androidResources { enable = true }
}
}
```
7. **Enable Java** compilation if module has `.java` source files:
```kotlin
kotlin {
android {
withJava()
}
}
```
8. **Enable tests** explicitly if the module has unit or instrumented tests:
```kotlin
kotlin {
android {
withHostTest { isIncludeAndroidResources = true }
withDeviceTest {
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
}
```
9. **Update Compose tooling dependency**:
```kotlin
// Old:
debugImplementation(libs.androidx.compose.ui.tooling)
// New:
androidRuntimeClasspath(libs.androidx.compose.ui.tooling)
```
10. **Publish consumer ProGuard rules** explicitly if applicable:
```kotlin
kotlin {
android {
consumerProguardFiles.add(file("consumer-rules.pro"))
}
}
```
11. **Resolve Sub-dependency Variants (Product Flavors / Build Types)**:
Because the new KMP Android library plugin enforces a single-variant architecture, it does not natively understand how to resolve dependencies that publish multiple variants (like `debug`/`release` build types, or product flavors like `free`/`paid`). Configure fallback behaviors using `localDependencySelection`:
```kotlin
kotlin {
android {
localDependencySelection {
// Determine which build type to consume from Android library dependencies, in order of preference
selectBuildTypeFrom.set(listOf("debug", "release"))
// If the dependency has a 'tier' dimension, select the 'free' flavor
productFlavorDimension("tier") {
selectFrom.set(listOf("free"))
}
}
}
}
```
## Path B: Android App + Shared Module Split
Use this when a module applies `kotlin.multiplatform` + `com.android.application`. This is **mandatory** for AGP 9.0 compatibility.
See [references/MIGRATION-APP-SPLIT.md](references/MIGRATION-APP-SPLIT.md) for full guide.
Summary:
1. **Create `androidApp` module** with its own `build.gradle.kts`:
```kotlin
plugins {
alias(libs.plugins.androidApplication)
// Do NOT apply kotlin-android — AGP 9.0 includes Kotlin support
alias(libs.plugins.composeMultiplatform) // if using Compose
alias(libs.plugins.composeCompiler) // if using Compose
}
android {
namespace = "com.example.app"
compileSdk = 35
defaultConfig {
applicationId = "com.example.app"
minSdk = 24
targetSdk = 35
versionCode = 1
versionName = "1.0"
}
buildFeatures { compose = true }
}
dependencies {
implementation(projects.shared) // or whatever the shared module is named
implementation(libs.androidx.activity.compose)
}
```
2. **Move Android entry point code** from `src/androidMain/` to `androidApp/src/main/`:
- `MainActivity.kt` (and any other Activities/Fragments)
- `AndroidManifest.xml` (app-level manifest with `<application>` and launcher `<activity>`) — verify `android:name` on `<activity>` uses the fully qualified class name in its new location
- Android Application class if present
- App-level resources (launcher icons, theme, etc.)
3. **Add to `settings.gradle.kts`**: `include(":androidApp")`
4. **Add to root `build.gradle.kts`**: plugin declarations with `apply false`
5. **Convert original module** from application to library using Path A steps
6. **Ensure different namespaces**: app module and library module must have distinct namespaces
7. **Remove from shared module**: `applicationId`, `targetSdk`, `versionCode`, `versionName`
8. **Update IDE run configurations**: change the module from the old module to `androidApp`
## Path C: Full Restructure (Recommended)
Use this when the project has a monolithic module (typically `composeApp`) containing entry
points for multiple platforms. This is optional but aligns with the new JetBrains default.
See [references/MIGRATION-FULL-RESTRUCTURE.md](references/MIGRATION-FULL-RESTRUCTURE.md) for full guide.
### Target Structure
```
project/
├── shared/ ← KMP library (was composeApp), pure shared code
├── androidApp/ ← Android entry point only
├── desktopApp/ ← Desktop entry point only (if desktop target exists)
├── webApp/ ← Wasm/JS entry point only (if web target exists)
├── iosApp/ ← iOS Xcode project (usually already separate)
└── ...
```
### Steps
1. **Apply Path B first** — extract `androidApp` (mandatory for AGP 9.0)
2. **Extract `desktopApp`** (if desktop target exists):
- Create module with `org.jetbrains.compose` and `application {}` plugin
- Move `main()` function from `desktopMain` to `desktopApp/src/main/kotlin/`
- Move `compose.desktop { application { ... } }` config to `desktopApp/build.gradle.kts`
- Add dependency on `shared` module
3. **Extract `webApp`** (if wasmJs/js target exists):
- Create module with appropriate Kotlin/JS or Kotlin/Wasm configuration
- Move web entry point from `wasmJsMain`/`jsMain` to `webApp/src/wasmJsMain/kotlin/`
- Move browser/distribution config to `webApp/build.gradle.kts`
- Add dependency on `shared` module
4. **iOS** — typically already in a separate `iosApp` directory. Verify:
- Framework export config (`binaries.framework`) stays in `shared` module
- Xcode project references the correct framework path
5. **Rename module** from `composeApp` to `shared`:
- Rename directory
- Update `settings.gradle.kts` include
- Update all dependency references across modules
6. **Clean up shared module**: remove all platform entry point code and app-specific config
that was moved to the platform app modules
### Variant: Native UI
If some platforms use native UI (e.g., SwiftUI for iOS), split `shared` into:
- `sharedLogic` — business logic consumed by ALL platforms
- `sharedUI` — Compose Multiplatform UI consumed only by platforms using shared UI
### Variant: Server
If the project includes a server target:
- Add `server` module at the root
- Move all client modules under an `app/` directory
- Add `core` module for code shared between server and client (models, validation)
## Version Updates
These are required regardless of migration path:
1. **Gradle wrapper** — update to 9.1.0+:
```properties
# gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
```
2. **AGP version** — update to 9.0.0+ and add the KMP library plugin.
With version catalog (`gradle/libs.versions.toml`):
```toml
[versions]
agp = "9.0.1"
[plugins]
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
```
Without version catalog — update `com.android.*` plugin versions and add in root `build.gradle.kts`:
```kotlin
plugins {
id("com.android.application") version "9.0.1" apply false
id("com.android.kotlin.multiplatform.library") version "9.0.1" apply false
}
```
3. **JDK** — ensure JDK 17+ is used (required by AGP 9.0)
4. **SDK Build Tools** — update to 36.0.0:
```
Install via SDK Manager or configure in android { buildToolsVersion = "36.0.0" }
```
5. **Review gradle.properties** — remove error-causing properties and review changed defaults (see "Gradle Properties Default Changes" section)
## Built-in Kotlin Migration
AGP 9.0 enables built-in Kotlin support by default for all `com.android.application` and `com.android.library`
modules. The `org.jetbrains.kotlin.android` plugin is no longer needed and will conflict if applied.
**Important:** Built-in Kotlin does NOT replace KMP support. KMP library modules still need
`org.jetbrains.kotlin.multiplatform` + `com.android.kotlin.multiplatform.library`.
### Step 1: Remove kotlin-android Plugin
Remove from **all** module-level and root-level build files:
```kotlin
// Remove from module build.gradle.kts
plugins {
// REMOVE: alias(libs.plugins.kotlin.android)
// REMOVE: id("org.jetbrains.kotlin.android")
}
// Remove from root build.gradle.kts
plugins {
// REMOVE: alias(libs.plugins.kotlin.android) apply false
}
```
Remove from version catalog (`gradle/libs.versions.toml`):
```toml
[plugins]
# REMOVE: kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
```
### Step 2: Migrate kapt to KSP or legacy-kapt
The `org.jetbrains.kotlin.kapt` plugin is **incompatible** with built-in Kotlin.
**Preferred: Migrate to KSP** — see the KSP migration guide for each annotation processor.
**Fallback: Use `com.android.legacy-kapt`** (same version as AGP):
```toml
# gradle/libs.versions.toml
[plugins]
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }
```
```kotlin
// Module build.gradle.kts — replace kotlin-kapt with legacy-kapt
plugins {
// REMOVE: alias(libs.plugins.kotlin.kapt)
alias(libs.plugins.legacy.kapt)
}
```
### Step 3: Migrate kotlinOptions to compilerOptions
For pure Android modules (non-KMP), migrate `android.kotlinOptions {}` to the top-level
`kotlin.compilerOptions {}`:
```kotlin
// Old
android {
kotlinOptions {
jvmTarget = "11"
languageVersion = "2.0"
freeCompilerArgs += listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
// New
kotlin {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_0)
optIn.add("kotlin.RequiresOptIn")
}
}
```
**Note:** With built-in Kotlin, `jvmTarget` defaults to `android.compileOptions.targetCompatibility`, so it may be optional if you already set `compileOptions`.
### Step 4: Migrate kotlin.sourceSets to android.sourceSets
With built-in Kotlin, only `android.sourceSets {}` with the `kotlin` set is supported:
```kotlin
// NOT SUPPORTED with built-in Kotlin:
kotlin.sourceSets.named("main") {
kotlin.srcDir("additionalSourceDirectory/kotlin")
}
// Correct:
android.sourceSets.named("main") {
kotlin.directories += "additionalSourceDirectory/kotlin"
}
```
For generated sources, use the Variant API:
```kotlin
androidComponents.onVariants { variant ->
variant.sources.kotlin!!.addStaticSourceDirectory("additionalSourceDirectory/kotlin")
}
```
### Per-Module Migration Strategy
For large projects, migrate module-by-module:
1. Disable globally: `android.builtInKotlin=false` in `gradle.properties`
2. Enable per migrated module by applying the opt-in plugin:
```kotlin
plugins {
id("com.android.built-in-kotlin") version "AGP_VERSION"
}
```
3. Follow Steps 1-4 for that module
4. Once all modules are migrated, remove `android.builtInKotlin=false` and all `com.android.built-in-kotlin` plugins
### Optional: Disable Kotlin for Non-Kotlin Modules
For modules that contain **no Kotlin sources**, disable built-in Kotlin to save build time:
```kotlin
android {
enableKotlin = false
}
```
### Opt-Out (Temporary)
If blocked by plugin incompatibilities, opt out temporarily:
```properties
# gradle.properties
android.builtInKotlin=false
android.newDsl=false # also required if using new DSL opt-out
```
**Warning:** Ask the user if they want to opt out, and if so, remind them this is a temporary measure.
## Plugin Compatibility
See [references/PLUGIN-COMPATIBILITY.md](references/PLUGIN-COMPATIBILITY.md) for the full compatibility table with known compatible versions, opt-out flag workarounds, and broken plugins.
**Before migrating**, inventory all plugins in the project and check each against that table. If any plugin is broken without workaround, inform the user. If plugins need opt-out flags, add them to`gradle.properties` and note them as temporary workarounds.
## Gradle Properties Default Changes
AGP 9.0 changes the defaults for many Gradle properties. Check `gradle.properties` for any explicitly set values that may now conflict.
Key changes:
| Property | Old Default | New Default | Action |
|------------------------------------------------------|-------------|-------------|---------------------------------------------------|
| `android.uniquePackageNames` | `false` | `true` | Ensure each library has a unique namespace |
| `android.enableAppCompileTimeRClass` | `false` | `true` | Refactor `switch` on R fields to `if/else` |
| `android.defaults.buildfeatures.resvalues` | `true` | `false` | Enable `resValues = true` where needed |
| `android.defaults.buildfeatures.shaders` | `true` | `false` | Enable shaders where needed |
| `android.r8.optimizedResourceShrinking` | `false` | `true` | Review R8 keep rules |
| `android.r8.strictFullModeForKeepRules` | `false` | `true` | Update keep rules to be explicit |
| `android.proguard.failOnMissingFiles` | `false` | `true` | Remove invalid ProGuard file references |
| `android.r8.proguardAndroidTxt.disallowed` | `false` | `true` | Use `proguard-android-optimize.txt` only |
| `android.r8.globalOptionsInConsumerRules.disallowed` | `false` | `true` | Remove global options from library consumer rules |
| `android.sourceset.disallowProvider` | `false` | `true` | Use `Sources` API on androidComponents |
| `android.sdk.defaultTargetSdkToCompileSdkIfUnset` | `false` | `true` | Specify `targetSdk` explicitly |
| `android.onlyEnableUnitTestForTheTestedBuildType` | `false` | `true` | Only if testing non-default build types |
Check for and remove properties that now cause errors:
- `android.r8.integratedResourceShrinking` — removed, always on
- `android.enableNewResourceShrinker.preciseShrinking` — removed, always on
## Pure Android Tips
For non-KMP Android modules upgrading to AGP 9.0, follow the "Built-in Kotlin Migration" steps above,
then review the "Gradle Properties Default Changes" table. Additional changes:
- **Review new DSL interfaces** — `BaseExtension` is removed; use `CommonExtension` or specific extension types
- **Java default changed** from Java 8 to Java 11 — ensure `compileOptions` reflects this
## Verification
After migration, verify with the [checklist](assets/checklist.md). Key checks:
1. `./gradlew build` succeeds with no errors
2. All platform targets build successfully (Android, iOS via `xcodebuild`, Desktop, JS/Wasm)
3. `./gradlew :shared:allTests` and Android unit tests pass
4. No `com.android.library` or `com.android.application` in KMP modules
5. No `org.jetbrains.kotlin.android` in AGP 9.0 modules
6. Source sets use correct names (`androidMain`, `androidHostTest`, `androidDeviceTest`)
7. No deprecation warnings about variant API or DSL
## Common Issues
See [references/KNOWN-ISSUES.md](references/KNOWN-ISSUES.md) for details. Key gotchas:
### KMP Library Plugin Issues
- **BuildConfig unavailable** in library modules — use DI/`AppConfiguration` interface, or use [BuildKonfig](https://github.com/yshrsmz/BuildKonfig) or [gradle-buildconfig-plugin](https://github.com/gmazzo/gradle-buildconfig-plugin) for compile-time constants
- **No build variants** — single variant architecture; compile-time constants can use BuildKonfig/gradle-buildconfig-plugin flavors, but variant-specific dependencies/resources/signing must move to app module
- **NDK/JNI unsupported** in new plugin — extract to separate `com.android.library` module
- **Compose resources crash** without `androidResources { enable = true }`
- **Consumer ProGuard rules silently dropped** if not migrated to `consumerProguardFiles.add(file(...))` in new DSL
- **KSP** requires version 2.3.1+ for AGP 9.0 compatibility
### AGP 9.0 General Issues
- **BaseExtension removed** — convention plugins using old DSL types need rewriting to use `CommonExtension`
- **Variant APIs removed** — `applicationVariants`, `libraryVariants`, `variantFilter` replaced by `androidComponents`
- **Convention plugins** need refactoring — old `android {}` extension helpers are obsolete
## Reference Files
- [DSL Reference](references/DSL-REFERENCE.md) — side-by-side old→new DSL mapping
- [Version Matrix](references/VERSION-MATRIX.md) — AGP/Gradle/KGP/Compose/IDE compatibility
- [Plugin Compatibility](references/PLUGIN-COMPATIBILITY.md) — third-party plugin status and workarounds