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-FULL-RESTRUCTURE.md

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

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

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

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

Update 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:embedAndSignAppleFrameworkForXcode
  3. App struct name: If the SwiftUI @main struct was named after the old module (e.g., ComposeAppApp), rename it to something appropriate for your project.

  4. Framework search paths: Update Build Settings if they reference the old module directory path.

  5. Cocoapods (if used): Update the pod spec name:

    // shared/build.gradle.kts
    kotlin {
        cocoapods {
            name = "Shared"
            summary = "Shared KMP module"
            // ...
        }
    }

Module Rename: composeApp to shared

1. Rename the Directory

mv composeApp shared

2. 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.xml and related files

Variant: Native UI (sharedLogic + sharedUI Split)

For projects where each platform has its own native UI and only business logic is shared:

sharedLogic/                # Pure KMP library (no Compose)
  build.gradle.kts          # kotlin.multiplatform + com.android.kotlin.multiplatform.library
  src/
    commonMain/kotlin/      # ViewModels, repositories, models, networking
    androidMain/kotlin/     # Android-specific implementations
    iosMain/kotlin/         # iOS-specific implementations

sharedUI/                   # Optional: Compose Multiplatform UI
  build.gradle.kts          # kotlin.multiplatform + com.android.kotlin.multiplatform.library + compose
  src/
    commonMain/kotlin/      # Shared composables
    androidMain/kotlin/     # Android-specific composables

androidApp/                 # Native Android app
  build.gradle.kts
  src/main/                 # Android UI (Compose or XML), depends on sharedLogic (and optionally sharedUI)

iosApp/                     # Native iOS app (SwiftUI/UIKit)
  # Depends on sharedLogic framework

sharedLogic/build.gradle.kts

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 and application {} block
  • Create webApp/ with wasmJs target and binaries.executable()
  • Move main() functions from composeApp/src/{platform}Main/ to respective app modules
  • Move compose.desktop.application {} config to desktopApp
  • Move wasmJs { browser {} } config to webApp
  • Rename composeApp to shared
  • Convert shared to KMP library plugin (com.android.kotlin.multiplatform.library)
  • Remove platform app targets from shared (keep only library targets)
  • Update all settings.gradle.kts includes
  • Update all project(":composeApp") references to project(":shared")
  • Update Xcode project (framework name, Gradle task path, Swift imports)
  • Verify each app module builds independently
  • Run all platform targets to confirm functionality

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.