Kmp project structure
Skill iammohdzaki/kmp-skills/skills/create-project/kmp-project-structure
Instead of copying and pasting the same prompts into every new project, you install this repo globally into your AI assistant. The AI learns exactly how to structure an AGP 9.0+ project, how to scaffold MVI architecture, how to resolve real library versions, and how to audit your Compose UI for Material 3 compliance.
npx -y skills add iammohdzaki/kmp-skills --skill kmp-project-structureAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Reference skill for the new recommended KMP project structure (2026+). Covers the shift from the old single-module composeApp layout to the new multi-module shared/ + androidApp/ + desktopApp/ structure required for AGP 9.0+. Includes full file and Gradle configuration for both old and new layouts.
SKILL.md
16.0 KB, as published. Nobody here has run it
KMP Project Structure (2026+)
IMPORTANT — Structure Changed
The KMP project structure has changed significantly. The old single-module layout where
composeAppheld both shared code and the Android app entry point is deprecated and incompatible with AGP 9.0+.Always scaffold new projects using the new multi-module layout described in this skill. Use the KMP Wizard to generate a reference project if unsure.
Old vs New Structure
Old Structure (pre-2026 / AGP < 9.0) — Do NOT use for new projects
MyApp/
├── composeApp/ ← ⚠️ Mixed: shared code + Android app entry point
│ ├── build.gradle.kts ← applies com.android.application + kotlin.multiplatform
│ └── src/
│ ├── commonMain/kotlin/…
│ ├── androidMain/kotlin/… ← MainActivity lives here
│ └── jvmMain/kotlin/…
├── iosApp/ ← Xcode project
├── gradle/libs.versions.toml
├── build.gradle.kts
└── settings.gradle.kts
Problem: AGP 9.0 forbids com.android.application (or com.android.library) from
coexisting with org.jetbrains.kotlin.multiplatform in the same module.
New Structure (AGP 9.0+ / KMP 2.2+) — Use this for all new projects
MyApp/
├── shared/ ← KMP library module (all shared code + shared UI)
│ ├── build.gradle.kts ← applies com.android.kotlin.multiplatform.library + kotlin.multiplatform
│ └── src/
│ ├── commonMain/kotlin/<pkg>/
│ │ ├── App.kt ← Root @Composable (shared UI)
│ │ ├── mvi/
│ │ ├── di/
│ │ └── navigation/
│ ├── androidMain/kotlin/<pkg>/ ← Android-specific actual implementations only
│ └── jvmMain/kotlin/<pkg>/ ← Desktop-specific actual implementations only
│
├── androidApp/ ← Thin Android shell (entry point only)
│ ├── build.gradle.kts ← applies com.android.application only
│ └── src/main/kotlin/<pkg>/
│ └── MainActivity.kt ← calls setContent { App() } — nothing more
│
├── desktopApp/ ← (optional) Thin Desktop shell
│ ├── build.gradle.kts ← applies org.jetbrains.kotlin.jvm
│ └── src/main/kotlin/
│ └── main.kt ← calls application { Window { App() } }
│
├── iosApp/ ← Xcode project (unchanged)
│ └── …
│
├── gradle/
│ └── libs.versions.toml
├── build.gradle.kts ← root build file
├── settings.gradle.kts
└── gradle.properties
Key principle: shared contains everything. App modules (androidApp, desktopApp)
are thin shells — they only wire the platform entry point to shared.
Module Responsibilities
| Module | Role | Gradle Plugins |
|---|---|---|
shared | All KMP code: common, Android, Desktop, iOS actuals | com.android.kotlin.multiplatform.library + org.jetbrains.kotlin.multiplatform + org.jetbrains.compose |
androidApp | Android entry point only (MainActivity) | com.android.application + org.jetbrains.kotlin.android |
desktopApp | Desktop entry point only (main.kt) | org.jetbrains.kotlin.jvm |
iosApp | Xcode project, Swift glue | N/A (Xcode project) |
File Contents
settings.gradle.kts
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyApp"
include(":shared")
include(":androidApp")
include(":desktopApp") // remove if Desktop not needed
Root build.gradle.kts
plugins {
// Declare plugins here but don't apply them at root level
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidKmpLibrary) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinSerialization) apply false
}
shared/build.gradle.kts
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidKmpLibrary) // com.android.kotlin.multiplatform.library
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
alias(libs.plugins.kotlinSerialization)
}
kotlin {
// Android target — uses the new android{} block (NOT androidTarget{})
android {
compilations.all {
compileTaskProvider.configure {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
}
}
// Desktop target (optional)
jvm("desktop")
// iOS targets (optional — add only if targeting iOS)
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "shared"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
// Compose Multiplatform — use libs.* (compose.* accessors deprecated)
implementation(libs.compose.runtime)
implementation(libs.compose.foundation)
implementation(libs.compose.ui)
implementation(libs.compose.material3)
implementation(libs.compose.material.icons.core)
// Kotlin
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.lifecycle.viewmodel)
// Add DI, navigation, image loading here based on user choice
}
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
}
val desktopMain by getting {
dependencies {
implementation(compose.desktop.currentOs)
}
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
}
}
// AGP 9+ — configure via android {} block from com.android.kotlin.multiplatform.library
android {
namespace = "com.<yourpackage>.shared"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
}
androidApp/build.gradle.kts
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.composeCompiler)
// Note: NO kotlinMultiplatform plugin here — this is a plain Android app
kotlin("android")
}
android {
namespace = "com.<yourpackage>"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
applicationId = "com.<yourpackage>"
minSdk = libs.versions.android.minSdk.get().toInt()
targetSdk = libs.versions.android.targetSdk.get().toInt()
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
dependencies {
implementation(project(":shared"))
implementation(libs.androidx.activity.compose)
}
androidApp/src/main/kotlin/<pkg>/MainActivity.kt
package com.<yourpackage>
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
// App() lives in shared — androidApp knows nothing about screens
App()
}
}
}
desktopApp/build.gradle.kts (optional)
plugins {
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
kotlin("jvm")
}
dependencies {
implementation(project(":shared"))
implementation(compose.desktop.currentOs)
}
compose.desktop {
application {
mainClass = "com.<yourpackage>.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 = "MyApp"
packageVersion = "1.0.0"
}
}
}
desktopApp/src/main/kotlin/main.kt (optional)
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "MyApp"
) {
// App() lives in shared
App()
}
}
shared/src/commonMain/kotlin/<pkg>/App.kt
package com.<yourpackage>
import androidx.compose.runtime.Composable
@Composable
fun App() {
AppTheme {
// AppNavigation() or first screen here
}
}
gradle/libs.versions.toml
⚠️ Use the
kmp-versionsskill to fill in real version numbers — do not hardcode from memory.
[versions]
# Fetch versions from official sources — see kmp-versions skill
kotlin = "FETCH"
agp = "FETCH"
composeMultiplatform = "FETCH"
coroutines = "FETCH"
serialization = "FETCH"
lifecycle = "FETCH"
android-compileSdk = "35"
android-targetSdk = "35"
android-minSdk = "24"
[libraries]
# Compose Multiplatform — explicit declarations (compose.* accessors deprecated in CMP 1.10)
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMultiplatform" }
compose-material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "composeMultiplatform" }
compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
# Kotlin / KMP
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization" }
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
# Android
androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "1.10.1" }
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
# AGP 9+ new plugin for shared KMP library module
androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
DSL Migration: androidTarget → android
With Kotlin ≥ 2.3.0 and the com.android.kotlin.multiplatform.library plugin, the Android
target DSL block is renamed:
// ❌ Old — deprecated since Kotlin 2.3.0, warns; removed in future
kotlin {
androidTarget {
compilations.all { … }
}
}
// ✅ New — use android {} when using com.android.kotlin.multiplatform.library
kotlin {
android {
compilations.all { … }
}
}
Source Set Naming
Source set names are unchanged — only the Gradle module structure changed:
| Source Set | Location in shared/ | Contains |
|---|---|---|
commonMain | src/commonMain/kotlin/ | All shared business logic, shared UI, ViewModels |
androidMain | src/androidMain/kotlin/ | Android actual implementations, platform utils |
jvmMain | src/jvmMain/kotlin/ | Desktop actual implementations |
iosMain | src/iosMain/kotlin/ | iOS actual implementations |
commonTest | src/commonTest/kotlin/ | Shared unit tests |
What Goes Where
| Code | Module | Source Set |
|---|---|---|
@Composable fun App() | shared | commonMain |
AppTheme, MaterialTheme | shared | commonMain |
| ViewModels, repositories, use-cases | shared | commonMain |
expect/actual Platform APIs | shared | commonMain / androidMain / jvmMain |
| DI modules (Koin) | shared | commonMain (shared) + androidMain/jvmMain (platform) |
Navigation (NavHost) | shared | commonMain |
MainActivity (Android entry point) | androidApp | main |
main.kt (Desktop entry point) | desktopApp | main |
Application class (if Koin on Android) | androidApp | main |
Migration Checklist (old → new structure)
- Create
shared/module — move allcomposeApp/src/contents here - Create
androidApp/module — moveMainActivity+Applicationhere - Update
shared/build.gradle.kts— replacecom.android.librarywithcom.android.kotlin.multiplatform.library - Rename
androidTarget {}toandroid {}inshared/build.gradle.kts - Update
androidApp/build.gradle.kts—com.android.applicationonly, depend on:shared - Update
settings.gradle.kts— include:shared,:androidApp,:desktopApp - Remove
compose.*plugin accessor shorthands — replace withlibs.* - Add
material-icons-coreexplicitly (no longer transitive since CMP 1.8.2) - Update all
importstatements for moved classes
Official References
- KMP Wizard — generates a valid project in the new structure
- Android KMP Library Plugin
- AGP 9 Migration Guide
- KMP Compatibility Guide
- New Default KMP Structure (JetBrains Blog)