479 lines
12 KiB
Markdown
479 lines
12 KiB
Markdown
# 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
|
|
|
|
```kotlin
|
|
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:
|
|
|
|
```kotlin
|
|
// 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
|
|
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
|
|
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
|
|
|
|
```kotlin
|
|
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:
|
|
|
|
```kotlin
|
|
// 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
|
|
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
|
|
|
|
```kotlin
|
|
// 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):
|
|
```bash
|
|
# 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:
|
|
```kotlin
|
|
// shared/build.gradle.kts
|
|
kotlin {
|
|
cocoapods {
|
|
name = "Shared"
|
|
summary = "Shared KMP module"
|
|
// ...
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Module Rename: composeApp to shared
|
|
|
|
### 1. Rename the Directory
|
|
|
|
```bash
|
|
mv composeApp shared
|
|
```
|
|
|
|
### 2. Update settings.gradle.kts
|
|
|
|
```kotlin
|
|
// 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`:
|
|
|
|
```kotlin
|
|
// 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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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
|
|
|
|
```kotlin
|
|
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
|