
Kotlin Tooling Agp9 Migration
- 1.2k installs
- 983 repo stars
- Updated July 21, 2026
- kotlin/kotlin-agent-skills
kotlin-tooling-agp9-migration is a Claude Code skill that runs a structured verification checklist for developers who migrated a Kotlin Multiplatform project to Android Gradle Plugin 9.0 and need to confirm build files a
About
kotlin-tooling-agp9-migration is a Kotlin agent skill that provides a post-migration verification checklist for Kotlin Multiplatform projects moving to Android Gradle Plugin 9.0. The checklist confirms com.android.kotlin.multiplatform.library is declared, legacy com.android.library and com.android.application plugins are removed from KMP modules, org.jetbrains.kotlin.android is dropped in favor of built-in Kotlin, and kapt is migrated to KSP or com.android.legacy-kapt. Developers reach for kotlin-tooling-agp9-migration immediately after AGP 9.0 migration to catch misconfigured kotlinOptions, kotlin.sourceSets, and version catalog entries before CI breaks. The skill is narrowly scoped to verification, not the migration steps themselves.
- 18-point post-migration verification checklist
- Covers plugin configuration, KMP library modules, source set renames, and dependency rules
- Flags deprecated Android plugin usage and incorrect Kotlin configuration
- Ensures compatibility between KSP, legacy kapt, and Compose Multiplatform resources
- Provides clear remediation guidance for each failed checklist item
Kotlin Tooling Agp9 Migration by the numbers
- 1,206 all-time installs (skills.sh)
- +68 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #928 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/kotlin/kotlin-agent-skills --skill kotlin-tooling-agp9-migrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.2k |
|---|---|
| repo stars | ★ 983 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 21, 2026 |
| Repository | kotlin/kotlin-agent-skills ↗ |
How do you verify KMP AGP 9.0 migration?
Run a structured verification checklist after migrating a Kotlin Multiplatform project to Android Gradle Plugin 9.0.
Who is it for?
Android and KMP engineers who just upgraded to Android Gradle Plugin 9.0 and need a systematic build-file audit before merging.
Skip if: Greenfield KMP projects still on AGP 8.x or teams not using Kotlin Multiplatform Android library modules.
When should I use this skill?
The user migrated to AGP 9.0, mentions com.android.kotlin.multiplatform.library, or needs to verify KMP plugin and kapt/KSP configuration after upgrade.
What you get
Checked plugin declarations, removed legacy Android/Kotlin plugins, and confirmed KSP or legacy-kapt and source set migrations in build files.
- Verified build.gradle.kts plugin list
- Migration checklist sign-off notes
By the numbers
- Targets Android Gradle Plugin 9.0 migration verification
- Covers kapt to KSP or com.android.legacy-kapt migration checks
Files
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 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 {
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/androidMainsrc/test→src/androidHostTestsrc/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 {
sourceSets {
androidMain.dependencies {
implementation("androidx.appcompat:appcompat:1.7.0")
}
}
}6. Enable resources explicitly if the module uses Android or Compose Multiplatform resources:
kotlin {
android {
androidResources { enable = true }
}
}7. Enable Java compilation if module has .java source files:
kotlin {
android {
withJava()
}
}8. Enable tests explicitly if the module has unit or instrumented tests:
kotlin {
android {
withHostTest { isIncludeAndroidResources = true }
withDeviceTest {
instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}
}9. Update Compose tooling dependency:
// Old:
debugImplementation(libs.androidx.compose.ui.tooling)
// New:
androidRuntimeClasspath(libs.androidx.compose.ui.tooling)10. Publish consumer ProGuard rules explicitly if applicable:
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 {
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 for full guide.
Summary:
1. Create `androidApp` module with its own build.gradle.kts:
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>) — verifyandroid:nameon<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 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.composeandapplication {}plugin - Move
main()function fromdesktopMaintodesktopApp/src/main/kotlin/ - Move
compose.desktop { application { ... } }config todesktopApp/build.gradle.kts - Add dependency on
sharedmodule
3. Extract `webApp` (if wasmJs/js target exists):
- Create module with appropriate Kotlin/JS or Kotlin/Wasm configuration
- Move web entry point from
wasmJsMain/jsMaintowebApp/src/wasmJsMain/kotlin/ - Move browser/distribution config to
webApp/build.gradle.kts - Add dependency on
sharedmodule
4. iOS — typically already in a separate iosApp directory. Verify:
- Framework export config (
binaries.framework) stays insharedmodule - Xcode project references the correct framework path
5. Rename module from composeApp to shared:
- Rename directory
- Update
settings.gradle.ktsinclude - 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 platformssharedUI— Compose Multiplatform UI consumed only by platforms using shared UI
Variant: Server
If the project includes a server target:
- Add
servermodule at the root - Move all client modules under an
app/directory - Add
coremodule 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+:
# gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip2. AGP version — update to 9.0.0+ and add the KMP library plugin.
With version catalog (gradle/libs.versions.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:
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:
// 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):
[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):
# gradle/libs.versions.toml
[plugins]
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }// 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 {}:
// 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:
// 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:
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:
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:
android {
enableKotlin = false
}Opt-Out (Temporary)
If blocked by plugin incompatibilities, opt out temporarily:
# gradle.properties
android.builtInKotlin=false
android.newDsl=false # also required if using new DSL opt-outWarning: 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 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 togradle.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 onandroid.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 —
BaseExtensionis removed; useCommonExtensionor specific extension types - Java default changed from Java 8 to Java 11 — ensure
compileOptionsreflects this
Verification
After migration, verify with the checklist. 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 for details. Key gotchas:
KMP Library Plugin Issues
- BuildConfig unavailable in library modules — use DI/
AppConfigurationinterface, or use BuildKonfig or 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.librarymodule - 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,variantFilterreplaced byandroidComponents - Convention plugins need refactoring — old
android {}extension helpers are obsolete
Reference Files
- DSL Reference — side-by-side old→new DSL mapping
- Version Matrix — AGP/Gradle/KGP/Compose/IDE compatibility
- Plugin Compatibility — third-party plugin status and workarounds
KMP AGP 9.0 Migration Verification Checklist
Use this checklist after migration to verify everything is configured correctly.
Plugin Configuration
- [ ]
com.android.kotlin.multiplatform.libraryplugin declared for KMP library modules - [ ] No
com.android.libraryorcom.android.applicationin KMP modules' build.gradle.kts - [ ]
org.jetbrains.kotlin.androidremoved from all build files and version catalog (built-in Kotlin replaces it) - [ ] No
org.jetbrains.kotlin.kaptplugin — migrated to KSP orcom.android.legacy-kapt - [ ]
android.kotlinOptions {}migrated tokotlin { compilerOptions {} }(non-KMP modules) - [ ]
kotlin.sourceSetsmigrated toandroid.sourceSetswith.kotlinaccessor (non-KMP modules) - [ ] No
android.builtInKotlin=falseunless 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 — useandroidLibrary {}insidekotlin {}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
debugImplementationor analogs in library modules - use
androidRuntimeClasspathfor tooling deps - app modules can still use
debugImplementation - [ ] Unique
namespacefor each library module (different from app module;android.uniquePackageNames=trueis default in AGP 9.0)
Gradle Properties & DSL
- [ ] No removed properties in
gradle.propertiesthat cause errors: android.enableLegacyVariantApiandroid.r8.integratedResourceShrinkingandroid.enableNewResourceShrinker.preciseShrinking- [ ] Any opt-out flags (
android.newDsl=false,android.builtInKotlin=false) documented with reason - [ ]
targetSdkexplicitly set in all app modules (defaults tocompileSdknow, wasminSdk)
Build Logic / Convention Plugins
- [ ] No references to
BaseExtension,AppExtension,LibraryExtension(removed in AGP 9.0) - [ ] Using
CommonExtensionor 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(notproguard-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 buildsucceeds - [ ]
./gradlew :androidApp:assembleDebugsucceeds (if app module exists) - [ ]
xcodebuild -project iosApp/*.xcodeproj -scheme <scheme> -sdk iphonesimulator buildsucceeds (if iOS app exists) - [ ] Desktop app compiles:
./gradlew :desktopApp:runor equivalent (if desktop target exists) - [ ] Web/Wasm target compiles:
./gradlew :wasmJsApp:wasmJsBrowserDistributionor equivalent (if web target exists) - [ ]
./gradlew :shared:allTestssucceeds (or equivalent for KMP test tasks) - [ ]
./gradlew :androidApp:testDebugUnitTestsucceeds (if app module exists) - [ ] No deprecation warnings about variant API or DSL
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:
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
// 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:
// 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 {
android {
withJava() // Required to compile .java files in androidMain
}
}---
Quick Reference: Minimal Migration Template
// 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)
}
}
}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:
// 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):
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.
// 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:
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):
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.
// 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// 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/actualwith different actual implementations selected by DI based on runtime config. - For library-specific debug/release behavior, use the
AppConfigurationpattern 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 {
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:
// 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:
// 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/tosrc/androidHostTest/ - Rename
src/androidInstrumentedTest/tosrc/androidDeviceTest/ - Update
sourceSetsreferences inbuild.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:
// 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:
// 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:
# 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:
# gradle.properties
android.builtInKotlin=falseWarning: 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-androidand 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):
# gradle/libs.versions.toml
[plugins]
legacy-kapt = { id = "com.android.legacy-kapt", version.ref = "agp" }// 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.BaseExtensionFix:
// 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:
// 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 constructorsandroid.r8.proguardAndroidTxt.disallowed=true— onlyproguard-android-optimize.txtis supportedandroid.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.txtingetDefaultProguardFile() - Remove global options (
-dontobfuscate,-dontoptimize) from library consumer rules - New option:
-processkotlinnullchecks keep|remove_message|removeto control Kotlin null checks
---
17. Removed Features
Problem: Several features are removed in AGP 9.0 with no replacement:
- Embedded Wear OS app support —
wearAppconfigurations 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:
// 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:
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
---
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:
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.
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.applicationplugin bundles Kotlin compilation. Applyingorg.jetbrains.kotlin.androidwill 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
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.sharedvscom.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
rootProject.name = "MyProject"
include(":composeApp")
include(":iosApp") // if present as a Gradle moduleAfter
rootProject.name = "MyProject"
include(":shared")
include(":androidApp")
include(":iosApp")---
Root build.gradle.kts Changes
With Version Catalog
Before:
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:
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:
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:
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.ktUpdate MainActivity to call into shared code:
// 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.xmlImportant: 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:
<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.kt4. 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
commonMaincode (ViewModels, repositories, models, shared composables) - All
expect/actualdeclarations - All
iosMain,desktopMain,wasmJsMaincode - Framework export configuration (
binaries.framework) - Shared Android resources (strings, drawables used by shared composables)
- Shared Android-specific implementations (
actualfunctions)
---
Namespace Requirements
The namespace for each module must be unique:
// 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:
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:
# Old
cd "$SRCROOT/.."
./gradlew :composeApp:embedAndSignAppleFrameworkForXcode
# New
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode3. Update Swift imports — in all .swift files, change the framework import to match baseName:
// Old
import ComposeApp
// New
import Shared4. 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:
// 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 withbuild.gradle.kts - [ ] Move
MainActivityandApplicationclass toandroidApp - [ ] Move
AndroidManifest.xml(full manifest) toandroidApp - [ ] Move app-level resources (launcher icons, app theme) to
androidApp - [ ] Move ProGuard rules to
androidApp - [ ] Convert
composeApptosharedwith KMP library plugin - [ ] Remove application-only config (
applicationId,versionCode,buildTypes) from shared - [ ] Add
implementation(project(":shared"))to androidApp dependencies - [ ] Update
settings.gradle.ktsincludes - [ ] Update root
build.gradle.ktsplugin declarations - [ ] Ensure namespaces are different between modules
- [ ] Update Android Studio run configuration
- [ ] Update Xcode project if iOS target exists
- [ ] Run
./gradlew :androidApp:assembleDebugand./gradlew :shared:assembleto verify
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
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.ktUpdate to call shared code:
// 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 {
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 {
// 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
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.ktUpdate to call shared code:
// 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.htmlUpdate index.html if the output JS filename changed.
Remove WasmJS from shared
In shared/build.gradle.kts, remove the wasmJs {} target:
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
// 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):
# In Xcode Build Phases > Run Script
cd "$SRCROOT/.."
./gradlew :shared:embedAndSignAppleFrameworkForXcode3. 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:
// shared/build.gradle.kts
kotlin {
cocoapods {
name = "Shared"
summary = "Shared KMP module"
// ...
}
}---
Module Rename: composeApp to shared
1. Rename the Directory
mv composeApp shared2. Update settings.gradle.kts
// 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:
// 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.xmland 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 frameworksharedLogic/build.gradle.kts
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
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
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 andapplication {}block - [ ] Create
webApp/with wasmJs target andbinaries.executable() - [ ] Move
main()functions fromcomposeApp/src/{platform}Main/to respective app modules - [ ] Move
compose.desktop.application {}config todesktopApp - [ ] Move
wasmJs { browser {} }config towebApp - [ ] Rename
composeApptoshared - [ ] 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.ktsincludes - [ ] Update all
project(":composeApp")references toproject(":shared") - [ ] Update Xcode project (framework name, Gradle task path, Swift imports)
- [ ] Verify each app module builds independently
- [ ] Run all platform targets to confirm functionality
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)
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)
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:
[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:
[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):
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):
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):
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.library")
}After (module build.gradle.kts):
plugins {
id("org.jetbrains.kotlin.multiplatform")
id("com.android.kotlin.multiplatform.library")
}Key changes:
- The plugin ID changes from
com.android.librarytocom.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:
plugins {
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}After:
plugins {
alias(libs.plugins.androidKmpLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}Without Version Catalog
Before:
plugins {
id("com.android.library") version "8.7.3" apply false
id("org.jetbrains.kotlin.multiplatform") version "2.1.0" apply false
}After:
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 {
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 {
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 {
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 {
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)
android {
defaultConfig {
consumerProguardFiles("consumer-rules.pro")
}
}After (AGP 9.x)
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 {
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 {
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 {
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:
// 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
dependencies {
debugImplementation(libs.compose.ui.tooling)
releaseImplementation(libs.some.lib)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.runner)
}After
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
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 {
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 {
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
// 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
// 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.tomlif using version catalog, or directly in build files) - [ ] Replace plugin alias in
build.gradle.kts - [ ] Move
android {}block contents intokotlin { android {} } - [ ] Replace
androidTarget {}withandroid {} - [ ] Replace
kotlinOptionswithcompilerOptions - [ ] Enable
androidResourcesif 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
dependenciesto source set dependencies - [ ] Update convention plugins if applicable
- [ ] Rename test source dirs:
androidUnitTesttoandroidHostTest,androidInstrumentedTesttoandroidDeviceTest - [ ] Update root
build.gradle.ktsplugin declarations - [ ] Run
./gradlew :module:assembleto verify - [ ] Run
./gradlew :module:testAndroidHostTestif there are any android host tests or common tests - [ ] Run
./gradlew :module:assembleAndroidDeviceTestif there are any android device tests
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
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.applicationandcom.android.library(no separatekotlin-androidplugin needed). - Removes support for
com.android.application+org.jetbrains.kotlin.multiplatformin 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 —
BaseExtensionand legacy types removed. org.jetbrains.kotlin.kaptincompatible — use KSP orcom.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.
targetSdkdefaults tocompileSdkwhen not set (wasminSdk).- 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
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1-bin.zip---
Basic libs.versions.toml template
[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:
# 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#!/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 "========================================"
Related skills
How it compares
Use kotlin-tooling-agp9-migration for post-upgrade KMP verification rather than general Gradle troubleshooting unrelated to AGP 9.0.
FAQ
When should developers run kotlin-tooling-agp9-migration?
kotlin-tooling-agp9-migration runs after migrating a Kotlin Multiplatform project to Android Gradle Plugin 9.0 to verify plugins, build.gradle.kts files, and version catalog entries match the new AGP requirements.
What plugins does kotlin-tooling-agp9-migration expect on KMP modules?
kotlin-tooling-agp9-migration expects com.android.kotlin.multiplatform.library on KMP library modules and confirms com.android.library, com.android.application, and org.jetbrains.kotlin.android are removed.
Is Kotlin Tooling Agp9 Migration safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.