KMP AGP 9.0.0 - Android Application to Library Migration
Upgrading New KMP Project to AGP 9.0
This guide will take you through the process step-by-step.
It's designed to be verbose and also image heavy so newer programmers can easily follow everything and ensure they are doing all the steps correctly.
There are full listings for all the changes.
I used the PL Coding Video as a reference to help with this and strongly recommend taking a look. There is a GitHub repo linked under the video as well that you can inspect. My libs.toml file contains different entries so these are not directly compatible. I tried to follow the conventions from the default wizards and made as few changes as possible.
Note: Unfortunately, the longer code sections will look terrible on mobile. Formatting everything for mobile makes it look bad on a monitor. Please use a bigger screen if you need to read the longer code sections. You should be fine to read and understand everything without needing to see the code listings.
Start New Project
Experienced Android developers should be comfortable working with different project and package names.
Upgrade to AGP 9.0.0
Use the inbuilt wizard to upgrade to the AGP 9.0.0
You can upgrade directly from 8.11.2 to 9.0.0 from the Dropdown:
- Upgrade AGP dependency from 8.11.2 to 9.0.0
- Upgrade Gradle version to 9.1.0
- Enable resValues build feature
- Disable targetSdk defaults to compileSdk
- Disable App Compile-Time R Class
- Continue to allow in the main manifest
- Allow non-unique package names
- Enable Dependency Constraints
- Disable R8 Strict Mode for Keep Rules
- Disable R8 Optimized Resource Shrinking
- Disable built-in Kotlin support
- Preserve the old (internal) AGP Dsl APIs
Upgrade Details
Opening your gradle.properties (project properties), you should see a lot of these steps are taken care of for you. I believe this was the change that was made to the built in wizard as I think these setting allow everything to run without moving the Android content into its own module.
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
#Gradle
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true
android.defaults.buildfeatures.resvalues=true
android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
android.enableAppCompileTimeRClass=false
android.usesSdkInManifest.disallowed=false
android.uniquePackageNames=false
android.dependency.useConstraints=true
android.r8.strictFullModeForKeepRules=false
android.r8.optimizedResourceShrinking=false
android.builtInKotlin=false
android.newDsl=false
Your gradle-wrapper.properties should also have been updated:
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
I will put other full content here as well in case you want to compare for any reason, but you should be able to safely ignore these. We will need to make changes to these later for the App -> Library Android migration.
build.gradle.kts (Module: composeApp) - Click to reveal
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.composeHotReload)
}
kotlin {
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
jvm()
js {
browser()
binaries.executable()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
sourceSets {
androidMain.dependencies {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose)
}
commonMain.dependencies {
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.components.resources)
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
}
}
}
android {
namespace = "com.github.d9l9.tests.kmpnewprojecttoagp9"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.github.d9l9.tests.kmpnewprojecttoagp9"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
}
}
buildTypes {
getByName("release") {
isMinifyEnabled = false
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
debugImplementation(libs.compose.uiTooling)
}
compose.desktop {
application {
mainClass = "com.github.d9l9.tests.kmpnewprojecttoagp9.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.github.d9l9.tests.kmpnewprojecttoagp9"
packageVersion = "1.0.0"
}
}
}
build.gradle.kts (Project: ProjectName) - Click to reveal
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.composeHotReload) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
}
libs.versions.toml - Click to reveal
[versions]
agp = "9.0.0"
android-compileSdk = "36"
android-minSdk = "24"
android-targetSdk = "36"
androidx-activity = "1.12.2"
androidx-appcompat = "1.7.1"
androidx-core = "1.17.0"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.9.6"
androidx-testExt = "1.3.0"
composeHotReload = "1.0.0"
composeMultiplatform = "1.10.0"
junit = "4.13.2"
kotlin = "2.3.0"
kotlinx-coroutines = "1.10.2"
material3 = "1.10.0-alpha05"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Run!
As well as Android, I'm also testing the JS, JVM, and wasmJs builds just to be safe. They all work, but I do get a lot of warnings with the JS and wasmJs builds (which are both also extremely slow). I'm running Windows on an Intel PC and so cannot test the iOS build locally. I can only assume it works.
Migrating Android to a Library
1. Locate Android Section
2. Create Module
4. Fix Dependencies
A) libs.versions.toml.
[plugins]
...
#Added
androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
Final libs.versions.toml - Click to reveal
[versions]
agp = "9.0.0"
#android-compileSdk = "36"
#android-minSdk = "24"
#android-targetSdk = "36"
androidx-activity = "1.12.2"
androidx-appcompat = "1.7.1"
androidx-core = "1.17.0"
androidx-espresso = "3.7.0"
androidx-lifecycle = "2.9.6"
androidx-testExt = "1.3.0"
composeHotReload = "1.0.0"
composeMultiplatform = "1.10.0"
junit = "4.13.2"
kotlin = "2.3.0"
kotlinx-coroutines = "1.10.2"
material3 = "1.10.0-alpha05"
material = "1.13.0"
[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
junit = { module = "junit:junit", version.ref = "junit" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" }
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" }
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" }
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" }
compose-uiTooling = { module = "org.jetbrains.compose.ui:ui-tooling", version.ref = "composeMultiplatform" }
androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" }
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "material3" }
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "composeMultiplatform" }
compose-uiToolingPreview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
#Added
androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
You don't need to adjust the Gradle wrapper, but I pushed my version up to 9.3.0.
Final gradle-wrapper.properties - Click to reveal
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
We just need to remove the following:
#android.defaults.buildfeatures.resvalues=true
#android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
#android.enableAppCompileTimeRClass=false
#android.usesSdkInManifest.disallowed=false
#android.uniquePackageNames=false
#android.dependency.useConstraints=true
#android.r8.strictFullModeForKeepRules=false
#android.r8.optimizedResourceShrinking=false
#android.builtInKotlin=false
#android.newDsl=false
I believe those options are what allowed project to run when upgrading to AGP 9 without moving the Android code into a library.
Final gradle.properties (project properties) - Click to reveal
#Kotlin
kotlin.code.style=official
kotlin.daemon.jvmargs=-Xmx3072M
#Gradle
org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8
org.gradle.configuration-cache=true
org.gradle.caching=true
#Android
android.nonTransitiveRClass=true
android.useAndroidX=true
#android.defaults.buildfeatures.resvalues=true
#android.sdk.defaultTargetSdkToCompileSdkIfUnset=false
#android.enableAppCompileTimeRClass=false
#android.usesSdkInManifest.disallowed=false
#android.uniquePackageNames=false
#android.dependency.useConstraints=true
#android.r8.strictFullModeForKeepRules=false
#android.r8.optimizedResourceShrinking=false
#android.builtInKotlin=false
#android.newDsl=false
Now we need to make some big changes to the project build file.
We should remove the jvmTarget, androidApplication plugin, androidTarget, android section, and dependencies section.
We need to add androidKmpLibrary and an androidLibrary section. You'll need to make sure your package name is correct for that (you can copy it from your composeApp).
We also need to add enableAndroidResources to androidLibrary or we get a new exciting crash when we click the button. Unfortunately, this is not caught at compile time and will only show up when your Android app accesses resources. I imagine this will be quickly patched out.
org.jetbrains.compose.resources.MissingResourceException: Missing resource with path: composeResources/kmpnewprojecttoagp9.composeapp.generated.resources/drawable/compose-multiplatform.xml
Rather than show these changes, please refer to the full listing.
Final build.gradle.kts (Module: composeApp) - Click to reveal
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
//import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
// alias(libs.plugins.androidApplication)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.composeHotReload)
// Added
alias(libs.plugins.androidKmpLibrary)
}
kotlin {
// androidTarget {
// compilerOptions {
// jvmTarget.set(JvmTarget.JVM_11)
// }
// }
// Added
androidLibrary {
compileSdk = 36
minSdk = 24
namespace = "com.github.d9l9.tests.composeApp"
// // org.jetbrains.compose.resources.MissingResourceException: Missing resource with path: composeResources/gradeagp9updatetestarea.composeapp.generated.resources/drawable/compose-multiplatform.xml
// experimentalProperties["android.experimental.kmp.enableAndroidResources"] = true // Withiout this, Android app will crash with
}
listOf(
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
jvm()
js {
browser()
binaries.executable()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}
//
sourceSets {
androidMain.dependencies {
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose)
}
commonMain.dependencies {
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.material3)
implementation(libs.compose.ui)
implementation(libs.compose.components.resources)
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.lifecycle.viewmodelCompose)
implementation(libs.androidx.lifecycle.runtimeCompose)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
jvmMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.kotlinx.coroutinesSwing)
}
}
}
//android {
// namespace = "com.github.d9l9.tests.kmpnewprojecttoagp9"
// compileSdk = libs.versions.android.compileSdk.get().toInt()
//
// defaultConfig {
// applicationId = "com.github.d9l9.tests.kmpnewprojecttoagp9"
// minSdk = libs.versions.android.minSdk.get().toInt()
// targetSdk = libs.versions.android.targetSdk.get().toInt()
// versionCode = 1
// versionName = "1.0"
// }
// packaging {
// resources {
// excludes += "/META-INF/{AL2.0,LGPL2.1}"
// }
// }
// buildTypes {
// getByName("release") {
// isMinifyEnabled = false
// }
// }
// compileOptions {
// sourceCompatibility = JavaVersion.VERSION_11
// targetCompatibility = JavaVersion.VERSION_11
// }
//}
//
//dependencies {
// debugImplementation(libs.compose.uiTooling)
//}
compose.desktop {
application {
mainClass = "com.github.d9l9.tests.kmpnewprojecttoagp9.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
packageName = "com.github.d9l9.tests.kmpnewprojecttoagp9"
packageVersion = "1.0.0"
}
}
}
This is a lot easier than the previous file.
We need to remove androidLibrary plugin and consumerProguardFiles("consumer-rules.pro"). We should add androidApplication and composeCompiler plugins.
plugins {
// alias(libs.plugins.androidLibrary)
// Added
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeCompiler)
}
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// consumerProguardFiles("consumer-rules.pro")
}
The compileOptions section can also be safely removed; our jvmToolchain takes care of all this.
// compileOptions {
// sourceCompatibility = JavaVersion.VERSION_11
// targetCompatibility = JavaVersion.VERSION_11
// }
Final build.gradle.kts (Module: androidApp) - Click to reveal
plugins {
// alias(libs.plugins.androidLibrary)
// Added
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeCompiler)
}
android {
namespace = "com.github.d9l9.tests.androidapp"
compileSdk {
version = release(36)
}
defaultConfig {
minSdk = 24
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
// consumerProguardFiles("consumer-rules.pro")
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
// compileOptions {
// sourceCompatibility = JavaVersion.VERSION_11
// targetCompatibility = JavaVersion.VERSION_11
// }
}
dependencies {
// Added
implementation(projects.composeApp)
implementation(libs.compose.uiToolingPreview)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.testExt.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
The easiest; we just need to add androidKmpLibrary.
plugins {
...
// Added
alias(libs.plugins.androidKmpLibrary) apply false
}
Final build.gradle.kts (Project: ProjectName) - Click to reveal
plugins {
// this is necessary to avoid the plugins to be loaded multiple times
// in each subproject's classloader
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.composeHotReload) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
// Added
alias(libs.plugins.androidKmpLibrary) apply false
}
Comments
Post a Comment