# Kotlin Multiplatform

> When to activate: Kotlin Multiplatform, KMP, shared code, expect/actual, commonMain, iOS interop, Ktor, SQLDelight, KMP mobile

- Skill: `mattakushi432/kotlin-multiplatform` (Agent Skill)
- Install (CLI): `npx skillmds@latest add mattakushi432/kotlin-multiplatform`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mattakushi432/kotlin-multiplatform/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Mattakushi432 (https://skillmd.com/u/mattakushi432)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mattakushi432/kotlin-multiplatform

---

# Kotlin Multiplatform (KMP) Patterns

## Project Structure

```
shared/
├── commonMain/kotlin/
│   ├── data/
│   │   ├── UserRepository.kt        # interface
│   │   └── UserRepositoryImpl.kt    # implementation using Ktor + SQLDelight
│   ├── domain/
│   │   └── UserUseCase.kt
│   └── platform/
│       └── Platform.kt              # expect declarations
├── androidMain/kotlin/
│   └── platform/
│       └── PlatformAndroid.kt       # actual declarations
└── iosMain/kotlin/
    └── platform/
        └── PlatformIos.kt           # actual declarations

androidApp/   # Android app module
iosApp/       # Xcode project
```

## expect / actual

```kotlin
// commonMain
expect class Platform() {
    val name: String
}

expect fun currentTimeMillis(): Long

expect class FileStorage(path: String) {
    fun read(): String
    fun write(content: String)
}

// androidMain
actual class Platform actual constructor() {
    actual val name: String = "Android ${android.os.Build.VERSION.RELEASE}"
}

actual fun currentTimeMillis(): Long = System.currentTimeMillis()

// iosMain
actual class Platform actual constructor() {
    actual val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun currentTimeMillis(): Long = NSDate().timeIntervalSince1970.toLong() * 1000
```

## Ktor in commonMain

```kotlin
// commonMain — same HTTP client for all platforms
class UserApiClient(private val httpClient: HttpClient) {

    suspend fun getUser(id: Long): User = httpClient.get("/users/$id").body()

    suspend fun createUser(request: CreateUserRequest): User =
        httpClient.post("/users") {
            contentType(ContentType.Application.Json)
            setBody(request)
        }.body()
}

// Platform-specific engine injection
// androidMain:
val client = HttpClient(Android) { install(ContentNegotiation) { json() } }

// iosMain:
val client = HttpClient(Darwin) { install(ContentNegotiation) { json() } }
```

## SQLDelight

```sql
-- commonMain/sqldelight/User.sq
CREATE TABLE user (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL UNIQUE,
    created_at INTEGER NOT NULL
);

findById:
SELECT * FROM user WHERE id = :id;

insert:
INSERT INTO user(id, name, email, created_at) VALUES (?, ?, ?, ?);

findAll:
SELECT * FROM user ORDER BY created_at DESC;
```

```kotlin
// Usage in commonMain
class LocalUserDataSource(private val db: AppDatabase) {
    fun findById(id: Long): User? = db.userQueries.findById(id).executeAsOneOrNull()

    fun insert(user: User) = db.userQueries.insert(user.id, user.name, user.email, user.createdAt)
}

// Database factory — expect/actual for driver
expect fun createDatabase(name: String): AppDatabase

// androidMain
actual fun createDatabase(name: String): AppDatabase =
    AppDatabase(AndroidSqliteDriver(AppDatabase.Schema, context, "$name.db"))

// iosMain
actual fun createDatabase(name: String): AppDatabase =
    AppDatabase(NativeSqliteDriver(AppDatabase.Schema, "$name.db"))
```

## Shared ViewModel (KMP-friendly)

```kotlin
// commonMain — no Android dependency
open class UserViewModel(private val useCase: UserUseCase) : CoroutineViewModel() {

    private val _state = MutableStateFlow<UserState>(UserState.Loading)
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun loadUser(id: Long) = viewModelScope.launch {
        runCatching { useCase.getUser(id) }
            .onSuccess { _state.value = UserState.Success(it) }
            .onFailure { _state.value = UserState.Error(it.message ?: "Unknown error") }
    }
}

// iosMain — expose as ObservableObject via SKIE or KMMViewModel
```

## build.gradle.kts (KMP)

```kotlin
kotlin {
    androidTarget { compilations.all { kotlinOptions { jvmTarget = "17" } } }
    iosX64(); iosArm64(); iosSimulatorArm64()

    sourceSets {
        commonMain.dependencies {
            implementation(libs.ktor.client.core)
            implementation(libs.ktor.client.content.negotiation)
            implementation(libs.sqldelight.runtime)
            implementation(libs.kotlinx.coroutines.core)
        }
        androidMain.dependencies {
            implementation(libs.ktor.client.android)
            implementation(libs.sqldelight.android.driver)
        }
        iosMain.dependencies {
            implementation(libs.ktor.client.darwin)
            implementation(libs.sqldelight.native.driver)
        }
    }
}
```

## Key Rules
- Keep `commonMain` free of platform APIs — any platform-specific code must go through `expect/actual`
- Use `StateFlow` in shared ViewModels; iOS can collect it via SKIE (`@NativeCoroutines`) or `Combine` bridge
- SQLDelight generates type-safe Kotlin from SQL — write SQL first, then call generated queries
- Freeze objects only when using the old memory model; with the new default memory model (Kotlin 1.7.20+) freezing is largely unnecessary
- Prefer SKIE or KMMViewModel library for iOS ViewModel exposure — raw coroutine bridging to Swift is error-prone

