All skills
kotlin avatar

/kotlin-tooling-agp9-migration

@43f4771 official
by kotlinkotlin/kotlin-agent-skills1.1k stars
42

Migrates Kotlin Multiplatform (KMP) projects to Android Gradle Plugin 9.0+. Handles plugin replacement (com.android.kotlin.multiplatform.library), module splitting, DSL migration, and the new default project structure. Use when upgrading AGP, when build fails due to KMP+AGP incompatibility, or when the user mentions AGP 9.0, android multiplatform plugin, KMP migration, or com.android.kotlin.multiplatform.library.

Use this Skill: https://skilld.dev/gh/kotlin/kotlin-agent-skills/kotlin-tooling-agp9-migration

This session only. Nothing lands on disk.

referencesMIGRATION-APP-SPLIT.md

≈3.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

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.application plugin bundles Kotlin compilation. Applying org.jetbrains.kotlin.android will cause a conflict error.
  • buildTypes and productFlavors work here. The application plugin still supports full variant configuration.
  • Compose compiler plugin is applied separately (composeCompiler), or it can come from KGP 2.0+ if you use the compose compiler Gradle plugin.
  • Depends on :shared to access all shared KMP code.

shared/build.gradle.kts

plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidKmpLibrary)
    alias(libs.plugins.composeMultiplatform)
    alias(libs.plugins.composeCompiler)
}

kotlin {
    android {
        namespace = "com.example.shared"
        compileSdk = 35
        minSdk = 24

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

        androidResources { enable = true }
    }

    iosX64()
    iosArm64()
    iosSimulatorArm64()

    listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
        it.binaries.framework {
            baseName = "Shared"
            isStatic = true
        }
    }

    sourceSets {
        commonMain.dependencies {
            implementation(compose.runtime)
            implementation(compose.foundation)
            implementation(compose.material3)
            implementation(compose.ui)
            implementation(libs.kotlinx.coroutines.core)
        }
        androidMain.dependencies {
            // Android-specific shared dependencies only
        }
    }
}

Key Points for shared

  • Plugin is com.android.kotlin.multiplatform.library, not com.android.library.
  • Namespace must differ from androidApp. Use com.example.shared vs com.example.app.
  • No applicationId, versionCode, versionName, targetSdk. These are application-only concepts.
  • No buildTypes or productFlavors. The KMP library plugin produces a single variant.
  • Framework exports (binaries.framework) stay here since iOS depends on the shared module.
  • androidTarget {} is replaced with android {}.

settings.gradle.kts Changes

Before

rootProject.name = "MyProject"
include(":composeApp")
include(":iosApp") // if present as a Gradle module

After

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.kt

Update 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.xml

Important: After moving the manifest, verify the android:name attribute on <activity> points to the correct Activity class in its new location. If the old manifest relied on a default or short class name, you may need to use the fully qualified name:

<activity
    android:name="com.example.app.MainActivity"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

The shared module may still have a minimal manifest (auto-generated or containing just <manifest> with no <application>).

3. Application Class (if any)

composeApp/src/androidMain/kotlin/.../MyApplication.kt
  --> androidApp/src/main/kotlin/.../MyApplication.kt

4. App-Level Resources

  • Launcher icons (mipmap-*)
  • App theme definitions that reference applicationId
  • Splash screen resources
  • Navigation graphs (if not shared)
composeApp/src/androidMain/res/mipmap-*/
composeApp/src/androidMain/res/values/themes.xml  (app-level theme)
  --> androidApp/src/main/res/

5. ProGuard Rules

composeApp/proguard-rules.pro
  --> androidApp/proguard-rules.pro

What Stays in shared

  • All commonMain code (ViewModels, repositories, models, shared composables)
  • All expect/actual declarations
  • All iosMain, desktopMain, wasmJsMain code
  • Framework export configuration (binaries.framework)
  • Shared Android resources (strings, drawables used by shared composables)
  • Shared Android-specific implementations (actual functions)

Namespace Requirements

The namespace for each module must be unique:

// 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:embedAndSignAppleFrameworkForXcode
  3. Update Swift imports — in all .swift files, change the framework import to match baseName:

    // Old
    import ComposeApp
    
    // New
    import Shared
  4. Update the app struct name if it was tied to the old module name. The @main struct name in your SwiftUI app entry point is independent of the framework name, but if it referenced the old name, rename it:

    // Example: rename if it was called ComposeAppApp or similar
    @main
    struct MyApp: App {
        var body: some Scene {
            WindowGroup {
                ContentView()
            }
        }
    }
  5. Update framework search paths in Xcode Build Settings if they reference the old module directory path.


Quick Checklist

  • Create androidApp/ directory with build.gradle.kts
  • Move MainActivity and Application class to androidApp
  • Move AndroidManifest.xml (full manifest) to androidApp
  • Move app-level resources (launcher icons, app theme) to androidApp
  • Move ProGuard rules to androidApp
  • Convert composeApp to shared with KMP library plugin
  • Remove application-only config (applicationId, versionCode, buildTypes) from shared
  • Add implementation(project(":shared")) to androidApp dependencies
  • Update settings.gradle.kts includes
  • Update root build.gradle.kts plugin declarations
  • Ensure namespaces are different between modules
  • Update Android Studio run configuration
  • Update Xcode project if iOS target exists
  • Run ./gradlew :androidApp:assembleDebug and ./gradlew :shared:assemble to verify

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    This skill provides documentation and tools to assist in migrating Kotlin Multiplatform projects to Android Gradle Plugin (AGP) 9.0+. It includes guides for library modules, application splits, and a project analysis script. No security risks were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    5/10 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 43f4771. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 3 weeks ago.

Activeupdated 6 months ago
metadata
{
  "author": "JetBrains",
  "version": "1.0.0"
}
  • kotlin
  • kmp
  • agp
  • android-gradle-plugin
  • migrations
  • multiplatform
  • gradle
  • android

README badge

README badge for kotlin/kotlin-agent-skills/kotlin-tooling-agp9-migration

Migrates Kotlin Multiplatform projects to Android Gradle Plugin 9.0+, which requires separating the Android application plugin from KMP. Handles plugin replacement, DSL migration, module splitting, and directory structure updates for both library and application modules.

Generated from the current SKILL.md.

Does this skill handle all KMP+AGP migration scenarios?
The skill covers three main paths: library modules (Path A), mandatory app+shared splits for AGP 9.0 (Path B), and recommended full restructures from monolithic modules (Path C). It assumes Kotlin Multiplatform projects; pure Android projects need only AGP version updates.
Do I need to remove the kotlin-android plugin?
Yes. AGP 9.0 has built-in Kotlin support, so org.jetbrains.kotlin.android must be removed from any module using AGP 9.0 plugins.
What happens to KAPT dependencies?
The skill identifies KAPT usage as incompatible with AGP 9.0. You must migrate to KSP or use com.android.legacy-kapt; the skill guides detection but does not automate the migration.
Do I need to rename my source directories?
Only if your module uses classic Android layout (src/main, src/test, src/androidTest). If it already uses KMP layout (src/androidMain, etc.), no directory renames are needed.
What Gradle and AGP versions does this target?
Gradle 9.1.0+ and AGP 9.0.0+. The skill includes gradle-wrapper updates and version catalog examples for both.

Generated from the current SKILL.md. These answers refresh after source changes.