Files
ttstd_family_care/.agents/skills/kotlin-tooling-agp9-migration/references/MIGRATION-APP-SPLIT.md

14 KiB

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