12 KiB
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:
- Keep a
jvm()target in shared (without theapplication {}block) and use intermediate source sets. - Put all shared code in
commonMainand rely on the JVM dependency fromdesktopApp.
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:
-
Framework import: Change
import ComposeApptoimport Sharedin all.swiftfiles (must matchbaseNamein the framework config). -
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 -
App struct name: If the SwiftUI
@mainstruct was named after the old module (e.g.,ComposeAppApp), rename it to something appropriate for your project. -
Framework search paths: Update Build Settings if they reference the old module directory path.
-
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.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 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 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