kmp-project-structure — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited kmp-project-structure (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
IMPORTANT — Structure Changed
>
The KMP project structure has changed significantly. The old single-module layout where composeApp held 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.
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.ktsProblem: AGP 9.0 forbids com.android.application (or com.android.library) from coexisting with org.jetbrains.kotlin.multiplatform in the same module.
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.propertiesKey principle: shared contains everything. App modules (androidApp, desktopApp) are thin shells — they only wire the platform entry point to shared.
| 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) |
settings.gradle.ktspluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyApp"
include(":shared")
include(":androidApp")
include(":desktopApp") // remove if Desktop not neededbuild.gradle.ktsplugins {
// 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.ktsimport 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.ktsplugins {
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.ktpackage 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.ktpackage com.<yourpackage>
import androidx.compose.runtime.Composable
@Composable
fun App() {
AppTheme {
// AppNavigation() or first screen here
}
}gradle/libs.versions.toml⚠️ Use the kmp-versions skill 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" }androidTarget → androidWith 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 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 |
| 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 |
shared/ module — move all composeApp/src/ contents hereandroidApp/ module — move MainActivity + Application hereshared/build.gradle.kts — replace com.android.library with com.android.kotlin.multiplatform.libraryandroidTarget {} to android {} in shared/build.gradle.ktsandroidApp/build.gradle.kts — com.android.application only, depend on :sharedsettings.gradle.kts — include :shared, :androidApp, :desktopAppcompose.* plugin accessor shorthands — replace with libs.*material-icons-core explicitly (no longer transitive since CMP 1.8.2)import statements for moved classes~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.