kmp
4
总安装量
3
周安装量
#54561
全站排名
安装命令
npx skills add https://github.com/andvl1/claude-plugin --skill kmp
Agent 安装分布
opencode
3
gemini-cli
3
github-copilot
3
codex
3
kimi-cli
3
cursor
3
Skill 文档
Kotlin Multiplatform (KMP) Fundamentals
Kotlin Multiplatform enables sharing code across Android, iOS, Desktop, Web (WASM), and Server.
Project Structure
Multi-Module Architecture (Feature-based + api/impl)
your-project-admin/
âââ build.gradle.kts # Root build config
âââ settings.gradle.kts # Module includes
âââ gradle/libs.versions.toml # Version catalog
â
âââ core/
â âââ common/ # Utilities, Result types, extensions
â â âââ src/commonMain/kotlin/
â âââ data/ # Data abstractions, DataStore
â â âââ src/commonMain/kotlin/
â â âââ src/androidMain/kotlin/
â â âââ src/iosMain/kotlin/
â âââ database/ # Room (Android/iOS/JVM only)
â â âââ src/commonMain/kotlin/
â âââ network/ # Ktor client
â â âââ src/commonMain/kotlin/
â âââ ui/ # Design system, theme
â âââ src/commonMain/kotlin/
â
âââ feature/
â âââ auth/
â â âââ api/ # Public interfaces, models
â â â âââ src/commonMain/kotlin/
â â âââ impl/ # Implementation, UI
â â âââ src/commonMain/kotlin/
â âââ home/
â âââ api/
â âââ impl/
â
âââ composeApp/ # Platform entry points
â âââ src/commonMain/ # App composition, DI graph
â âââ src/androidMain/ # MainActivity
â âââ src/iosMain/ # iOS entry
â âââ src/jvmMain/ # Desktop main()
â âââ src/wasmJsMain/ # Web entry
â
âââ iosApp/ # Xcode project
Source Sets Hierarchy
commonMain
âââ androidMain
âââ iosMain
â âââ iosX64Main
â âââ iosArm64Main
â âââ iosSimulatorArm64Main
âââ jvmMain
âââ wasmJsMain
âââ jsMain (fallback)
Gradle Setup
Root build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.androidApplication) apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.composeMultiplatform) apply false
alias(libs.plugins.composeCompiler) apply false
alias(libs.plugins.kotlinSerialization) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.metro) apply false
}
settings.gradle.kts
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "your-project-admin"
// Core modules
include(":core:common")
include(":core:data")
include(":core:database")
include(":core:network")
include(":core:ui")
// Feature modules
include(":feature:auth:api")
include(":feature:auth:impl")
include(":feature:home:api")
include(":feature:home:impl")
// App entry points
include(":composeApp")
gradle/libs.versions.toml
[versions]
kotlin = "2.1.0"
agp = "8.7.3"
compose-multiplatform = "1.7.3"
ktor = "3.1.1"
room = "2.8.4"
datastore = "1.2.0"
decompose = "3.5.0"
metro = "0.1.1"
essenty = "2.5.0"
coroutines = "1.10.1"
serialization = "1.7.3"
ksp = "2.1.0-1.0.29"
[libraries]
# Kotlin
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" }
# Ktor
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
# Room (Android, iOS, JVM only)
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version = "2.6.2" }
# DataStore
datastore-preferences-core = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" }
# Decompose
decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" }
decompose-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" }
essenty-lifecycle = { module = "com.arkivanov.essenty:lifecycle", version.ref = "essenty" }
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
room = { id = "androidx.room", version.ref = "room" }
metro = { id = "dev.zacsweers.metro", version.ref = "metro" }
Module build.gradle.kts (KMP Library)
// core/common/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "CoreCommon"
isStatic = true
}
}
jvm("desktop")
@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
}
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
}
androidMain.dependencies {
// Android-specific
}
iosMain.dependencies {
// iOS-specific
}
val desktopMain by getting {
dependencies {
// Desktop-specific
}
}
}
}
android {
namespace = "com.your-project.admin.core.common"
compileSdk = 35
defaultConfig {
minSdk = 24
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
expect/actual Pattern
Declaration (commonMain)
// commonMain/kotlin/Platform.kt
expect class PlatformContext
expect fun getPlatformName(): String
expect fun createDataStorePath(context: PlatformContext): String
Android Implementation
// androidMain/kotlin/Platform.android.kt
actual typealias PlatformContext = android.content.Context
actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"
actual fun createDataStorePath(context: PlatformContext): String {
return context.filesDir.resolve("datastore").absolutePath
}
iOS Implementation
// iosMain/kotlin/Platform.ios.kt
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
actual class PlatformContext
actual fun getPlatformName(): String = "iOS"
actual fun createDataStorePath(context: PlatformContext): String {
val documentDir = NSFileManager.defaultManager.URLForDirectory(
NSDocumentDirectory,
NSUserDomainMask,
null,
false,
null
)
return "${documentDir?.path}/datastore"
}
Desktop Implementation
// desktopMain/kotlin/Platform.jvm.kt
import java.io.File
actual class PlatformContext
actual fun getPlatformName(): String =
"${System.getProperty("os.name")} ${System.getProperty("os.version")}"
actual fun createDataStorePath(context: PlatformContext): String {
val home = System.getProperty("user.home")
return File(home, ".your-project-admin/datastore").absolutePath
}
WASM Implementation
// wasmJsMain/kotlin/Platform.wasmJs.kt
actual class PlatformContext
actual fun getPlatformName(): String = "Web (WASM)"
actual fun createDataStorePath(context: PlatformContext): String {
return "your-project-admin-datastore" // Uses localStorage
}
Module Dependencies
Dependency Rules
composeApp
âââ feature:auth:impl
â âââ feature:auth:api
â âââ core:ui
â âââ core:network
âââ feature:home:impl
â âââ feature:home:api
â âââ core:ui
â âââ core:database
âââ core:ui
â âââ core:common
âââ core:network
â âââ core:common
âââ core:database
â âââ core:common
âââ core:data
âââ core:common
api/impl Pattern
// feature/auth/api/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
}
kotlin {
// Targets...
sourceSets {
commonMain.dependencies {
// Only models and interfaces - no implementations
api(projects.core.common)
}
}
}
// feature/auth/impl/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.composeMultiplatform)
alias(libs.plugins.composeCompiler)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.feature.auth.api)
implementation(projects.core.ui)
implementation(projects.core.network)
implementation(libs.decompose)
implementation(libs.decompose.compose)
}
}
}
Common Patterns
Result Type
// core/common/src/commonMain/kotlin/Result.kt
sealed class AppResult<out T> {
data class Success<T>(val data: T) : AppResult<T>()
data class Error(val message: String, val cause: Throwable? = null) : AppResult<Nothing>()
data object Loading : AppResult<Nothing>()
}
inline fun <T, R> AppResult<T>.map(transform: (T) -> R): AppResult<R> = when (this) {
is AppResult.Success -> AppResult.Success(transform(data))
is AppResult.Error -> this
is AppResult.Loading -> this
}
inline fun <T> AppResult<T>.onSuccess(action: (T) -> Unit): AppResult<T> {
if (this is AppResult.Success) action(data)
return this
}
inline fun <T> AppResult<T>.onError(action: (String, Throwable?) -> Unit): AppResult<T> {
if (this is AppResult.Error) action(message, cause)
return this
}
Repository Interface (api module)
// feature/auth/api/src/commonMain/kotlin/AuthRepository.kt
interface AuthRepository {
suspend fun login(email: String, password: String): AppResult<User>
suspend fun logout(): AppResult<Unit>
fun observeAuthState(): Flow<AuthState>
}
data class User(
val id: String,
val email: String,
val name: String
)
sealed class AuthState {
data object Unauthenticated : AuthState()
data class Authenticated(val user: User) : AuthState()
}
Repository Implementation (impl module)
// feature/auth/impl/src/commonMain/kotlin/AuthRepositoryImpl.kt
class AuthRepositoryImpl(
private val apiService: AuthApiService,
private val tokenStorage: TokenStorage
) : AuthRepository {
private val _authState = MutableStateFlow<AuthState>(AuthState.Unauthenticated)
override suspend fun login(email: String, password: String): AppResult<User> {
return try {
val response = apiService.login(LoginRequest(email, password))
tokenStorage.saveToken(response.token)
_authState.value = AuthState.Authenticated(response.user)
AppResult.Success(response.user)
} catch (e: Exception) {
AppResult.Error("Login failed: ${e.message}", e)
}
}
override suspend fun logout(): AppResult<Unit> {
tokenStorage.clearToken()
_authState.value = AuthState.Unauthenticated
return AppResult.Success(Unit)
}
override fun observeAuthState(): Flow<AuthState> = _authState.asStateFlow()
}
Platform Checks at Runtime
// When expect/actual is overkill, use runtime checks
enum class Platform {
Android, iOS, Desktop, Web
}
expect val currentPlatform: Platform
// androidMain
actual val currentPlatform: Platform = Platform.Android
// iosMain
actual val currentPlatform: Platform = Platform.iOS
// desktopMain
actual val currentPlatform: Platform = Platform.Desktop
// wasmJsMain
actual val currentPlatform: Platform = Platform.Web
// Usage
@Composable
fun AdaptiveComponent() {
when (currentPlatform) {
Platform.Android -> AndroidSpecificUI()
Platform.iOS -> IOSSpecificUI()
Platform.Desktop -> DesktopSpecificUI()
Platform.Web -> WebSpecificUI()
}
}
Testing
Shared Tests (commonTest)
// core/common/src/commonTest/kotlin/ResultTest.kt
class ResultTest {
@Test
fun `map transforms success value`() {
val result: AppResult<Int> = AppResult.Success(5)
val mapped = result.map { it * 2 }
assertTrue(mapped is AppResult.Success)
assertEquals(10, (mapped as AppResult.Success).data)
}
@Test
fun `map preserves error`() {
val result: AppResult<Int> = AppResult.Error("test error")
val mapped = result.map { it * 2 }
assertTrue(mapped is AppResult.Error)
}
}
Platform-Specific Tests
// androidTest - uses Robolectric or instrumented tests
// iosTest - runs on simulator
// jvmTest - standard JUnit
Best Practices
Do’s
- Put as much code as possible in
commonMain - Use expect/actual only for platform APIs
- Keep platform-specific implementations minimal
- Use dependency injection for platform differences
- Test shared code in
commonTest - Use version catalogs for dependencies
Don’ts
- Don’t put platform-specific code in common modules
- Don’t duplicate code across platform source sets
- Don’t use platform-specific types in public APIs
- Don’t skip proper module separation (api/impl)
- Don’t ignore WASM limitations for database
WASM Limitations
Important limitations when targeting WebAssembly:
Database
- Room: NOT supported on WASM
- Use
localStorageorIndexedDBvia expect/actual pattern - Consider skipping database for wasmJsMain source set
// commonMain - interface only
expect class AppStorage {
fun getString(key: String): String?
fun putString(key: String, value: String)
}
// wasmJsMain - browser storage
actual class AppStorage {
actual fun getString(key: String): String? =
window.localStorage.getItem(key)
actual fun putString(key: String, value: String) {
window.localStorage.setItem(key, value)
}
}
Network
- CORS: All HTTP requests subject to browser CORS policy
- WebSocket: Works but needs CORS-compatible server
File System
- No direct file system access
- Use virtual file APIs or blob URLs
Threading
- No
Dispatchers.IO(useDispatchers.Default) - Web Workers for background tasks (limited)