399 lines
8.4 KiB
Markdown
399 lines
8.4 KiB
Markdown
---
|
|
id: mobile-kmp-deep
|
|
title: Kotlin Multiplatform — shared business logic
|
|
category: Coding
|
|
status: draft
|
|
source_trust_level: B
|
|
verification_status: conceptual
|
|
created_at: 2026-05-09
|
|
updated_at: 2026-05-09
|
|
tags: [mobile, kmp, kotlin, vibe-coding]
|
|
tech_stack: { language: "Kotlin", applicable_to: ["iOS", "Android"] }
|
|
applied_in: []
|
|
aliases: [KMP, Kotlin Multiplatform, KMM, Compose Multiplatform, expect/actual, shared module]
|
|
---
|
|
|
|
# Kotlin Multiplatform Deep
|
|
|
|
> Kotlin 가 iOS + Android + Web + Desktop 동시. **Shared business logic + native UI**. KMM 가 mobile, Compose Multiplatform 가 UI 도. JetBrains official.
|
|
|
|
## 📖 핵심 개념
|
|
- Common code: Kotlin (shared).
|
|
- Platform-specific: expect / actual.
|
|
- iOS = Kotlin → Native (LLVM).
|
|
- Android = Kotlin → JVM (default).
|
|
|
|
## 💻 코드 패턴
|
|
|
|
### Project structure
|
|
```
|
|
my-app/
|
|
├── shared/ # Shared module
|
|
│ ├── commonMain/ # Common Kotlin
|
|
│ ├── androidMain/ # Android-only
|
|
│ ├── iosMain/ # iOS-only
|
|
│ └── jsMain/ # Web (optional)
|
|
├── androidApp/ # Android app
|
|
├── iosApp/ # iOS Xcode project
|
|
└── build.gradle.kts
|
|
```
|
|
|
|
### shared/build.gradle.kts
|
|
```kotlin
|
|
kotlin {
|
|
androidTarget()
|
|
iosX64()
|
|
iosArm64()
|
|
iosSimulatorArm64()
|
|
|
|
cocoapods {
|
|
summary = "Shared module"
|
|
homepage = "..."
|
|
ios.deploymentTarget = "16.0"
|
|
framework {
|
|
baseName = "shared"
|
|
isStatic = true
|
|
}
|
|
}
|
|
|
|
sourceSets {
|
|
commonMain.dependencies {
|
|
implementation("io.ktor:ktor-client-core:2.3.7")
|
|
implementation("io.ktor:ktor-client-content-negotiation:2.3.7")
|
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
|
}
|
|
androidMain.dependencies {
|
|
implementation("io.ktor:ktor-client-okhttp:2.3.7")
|
|
}
|
|
iosMain.dependencies {
|
|
implementation("io.ktor:ktor-client-darwin:2.3.7")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Shared model
|
|
```kotlin
|
|
// commonMain/User.kt
|
|
@Serializable
|
|
data class User(
|
|
val id: String,
|
|
val name: String,
|
|
val email: String,
|
|
)
|
|
```
|
|
|
|
### Shared API client
|
|
```kotlin
|
|
// commonMain/UserRepository.kt
|
|
class UserRepository(private val client: HttpClient) {
|
|
suspend fun fetchUser(id: String): User {
|
|
return client.get("https://api.example.com/users/$id").body()
|
|
}
|
|
}
|
|
```
|
|
|
|
### expect / actual
|
|
```kotlin
|
|
// commonMain/Platform.kt
|
|
expect fun platform(): String
|
|
|
|
// androidMain/Platform.kt
|
|
actual fun platform(): String = "Android ${android.os.Build.VERSION.SDK_INT}"
|
|
|
|
// iosMain/Platform.kt
|
|
import platform.UIKit.UIDevice
|
|
actual fun platform(): String = "iOS ${UIDevice.currentDevice.systemVersion}"
|
|
```
|
|
|
|
→ Common 가 abstract, platform 가 concrete.
|
|
|
|
### Network (Ktor)
|
|
```kotlin
|
|
// commonMain
|
|
val client = HttpClient {
|
|
install(ContentNegotiation) {
|
|
json(Json { ignoreUnknownKeys = true })
|
|
}
|
|
install(Logging) { level = LogLevel.HEADERS }
|
|
}
|
|
|
|
suspend fun fetchUsers(): List<User> {
|
|
return client.get("https://...").body()
|
|
}
|
|
```
|
|
|
|
→ HTTP / JSON / serialization 가 common.
|
|
|
|
### DB (SQLDelight)
|
|
```kotlin
|
|
// shared/build.gradle.kts
|
|
plugins {
|
|
id("app.cash.sqldelight") version "2.0.0"
|
|
}
|
|
|
|
sqldelight {
|
|
databases {
|
|
create("AppDb") {
|
|
packageName.set("com.example.shared.db")
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
```sql
|
|
-- shared/src/commonMain/sqldelight/com/example/User.sq
|
|
CREATE TABLE User (
|
|
id TEXT PRIMARY KEY,
|
|
name TEXT NOT NULL
|
|
);
|
|
|
|
selectAll:
|
|
SELECT * FROM User;
|
|
|
|
insert:
|
|
INSERT OR REPLACE INTO User VALUES (?, ?);
|
|
```
|
|
|
|
```kotlin
|
|
// commonMain
|
|
val driver: SqlDriver = ... // platform 별
|
|
val db = AppDb(driver)
|
|
db.userQueries.insert("1", "Alice")
|
|
val users = db.userQueries.selectAll().executeAsList()
|
|
```
|
|
|
|
→ Type-safe SQL. iOS / Android 둘 다.
|
|
|
|
### Coroutines + Flow
|
|
```kotlin
|
|
// commonMain
|
|
class UserViewModel(private val repo: UserRepository) {
|
|
val users: Flow<List<User>> = flow {
|
|
emit(repo.fetchUsers())
|
|
}
|
|
}
|
|
```
|
|
|
|
→ Flow 가 iOS 에서 도 사용 (KMP-NativeCoroutines).
|
|
|
|
### iOS 사용 (Swift)
|
|
```swift
|
|
// iosApp
|
|
import shared
|
|
|
|
class UserListVM: ObservableObject {
|
|
@Published var users: [User] = []
|
|
private let repo = UserRepository(client: ...)
|
|
|
|
func load() async {
|
|
do {
|
|
let result = try await repo.fetchUsers()
|
|
await MainActor.run { self.users = result }
|
|
} catch {
|
|
print(error)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
→ Kotlin class 가 Swift 에서 native 처럼.
|
|
|
|
### Async on iOS (KMP-NativeCoroutines)
|
|
```kotlin
|
|
// commonMain
|
|
@NativeCoroutines
|
|
suspend fun fetchUsers(): List<User> = ...
|
|
```
|
|
|
|
```swift
|
|
// iOS
|
|
import KMPNativeCoroutinesAsync
|
|
let users = try await asyncFunction(for: repo.fetchUsersNative())
|
|
```
|
|
|
|
→ Suspend → Swift async/await.
|
|
|
|
### Compose Multiplatform (UI도)
|
|
```kotlin
|
|
// commonMain (Compose UI)
|
|
@Composable
|
|
fun UserCard(user: User) {
|
|
Card {
|
|
Column(Modifier.padding(16.dp)) {
|
|
Text(user.name, style = MaterialTheme.typography.h6)
|
|
Text(user.email)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
→ iOS + Android 가 같은 UI.
|
|
→ iOS Compose 가 stable (2024+).
|
|
|
|
### vs SwiftUI / Jetpack Compose only
|
|
```
|
|
KMP shared logic + native UI:
|
|
- Native 친화 (각 platform UI)
|
|
- 작은 cost.
|
|
|
|
Compose Multiplatform:
|
|
- Single UI codebase
|
|
- iOS 도 Compose
|
|
- "iOS 답지" 의 약간
|
|
|
|
→ Logic 만 = KMP.
|
|
UI 도 = Compose Multiplatform.
|
|
```
|
|
|
|
### vs Flutter / React Native
|
|
```
|
|
Flutter / RN:
|
|
- Single 언어 (Dart / JS)
|
|
- Single UI codebase
|
|
- Native bridge
|
|
|
|
KMP:
|
|
- Native 언어 (Kotlin + Swift)
|
|
- Native UI (보통)
|
|
- Logic 만 share
|
|
|
|
→ KMP 가 native-friendly.
|
|
RN/Flutter 가 빠른 dev.
|
|
```
|
|
|
|
### Pros
|
|
```
|
|
- Native iOS UX.
|
|
- Logic share = bug 1번 fix.
|
|
- Type-safe (Kotlin).
|
|
- Coroutines (async).
|
|
- Native interop.
|
|
```
|
|
|
|
### Cons
|
|
```
|
|
- iOS dev 가 Kotlin 이해.
|
|
- Library 가 작음 (vs RN).
|
|
- Build time (LLVM iOS = 느린).
|
|
- Tooling 가 RN/Flutter 보다 약함.
|
|
```
|
|
|
|
### Real-world
|
|
- **Cash App** (Square): 큰 KMP user.
|
|
- **Netflix**: 일부 mobile.
|
|
- **Philips**: hue lighting.
|
|
- **VMware**: Workspace ONE.
|
|
- **Touchlab**: KMM consultancy.
|
|
|
|
### LiveData / StateFlow → Swift
|
|
```kotlin
|
|
// commonMain
|
|
class ViewModel {
|
|
val state: StateFlow<UserState> = ...
|
|
}
|
|
```
|
|
|
|
```swift
|
|
// iOS
|
|
ViewModel().state.collect { state in
|
|
// ...
|
|
}
|
|
```
|
|
|
|
→ StateFlow 가 Combine publisher 처럼.
|
|
|
|
### Image loading (Coil 3 multiplatform)
|
|
```kotlin
|
|
// Compose Multiplatform
|
|
AsyncImage(
|
|
model = "https://...",
|
|
contentDescription = null,
|
|
)
|
|
```
|
|
|
|
### Settings (Multiplatform)
|
|
```kotlin
|
|
// com.russhwolf:multiplatform-settings
|
|
val settings: Settings = ...
|
|
settings.putString("theme", "dark")
|
|
val theme = settings.getString("theme", "light")
|
|
```
|
|
|
|
### Test
|
|
```kotlin
|
|
// commonTest
|
|
class UserRepoTest {
|
|
@Test
|
|
fun fetchesUser() = runTest {
|
|
val user = repo.fetchUser("1")
|
|
assertEquals("Alice", user.name)
|
|
}
|
|
}
|
|
```
|
|
|
|
→ Common test 가 모든 platform.
|
|
|
|
### Migration to KMP
|
|
```
|
|
1. Existing Android app.
|
|
2. shared module 추가.
|
|
3. 1 logic (e.g. API client) 이동.
|
|
4. iOS 가 framework 사용.
|
|
5. 점진 — 1 module 씩.
|
|
```
|
|
|
|
→ Strangler fig 식.
|
|
|
|
### Build
|
|
```bash
|
|
# Gradle (Android)
|
|
./gradlew :androidApp:installDebug
|
|
|
|
# iOS
|
|
cd iosApp && pod install
|
|
xed iosApp.xcworkspace
|
|
# Xcode build & run.
|
|
|
|
# Release framework
|
|
./gradlew :shared:linkReleaseFrameworkIosArm64
|
|
```
|
|
|
|
### Future
|
|
```
|
|
- Compose Multiplatform iOS 가 stable + 인기 ↑.
|
|
- KMP 의 toolchain 빠름.
|
|
- More libraries multiplatform.
|
|
- "Kotlin everywhere".
|
|
```
|
|
|
|
## 🤔 의사결정 기준
|
|
| 상황 | 추천 |
|
|
|---|---|
|
|
| Native UX 중요 + share logic | KMP (Kotlin shared, Swift/Kotlin UI) |
|
|
| Single UI codebase | Compose Multiplatform |
|
|
| 빠른 prototype | RN / Flutter |
|
|
| 큰 회사 + Android 강 | KMP |
|
|
| iOS dev 가 Swift 만 | KMP shared logic only |
|
|
| Web 도 share | KMP / RN Web |
|
|
| Performance critical | Native or KMP |
|
|
|
|
## ❌ 안티패턴
|
|
- **모든 거 share (UI 까지)**: native UX 잃음.
|
|
- **Platform-specific 가 common**: leak.
|
|
- **Coroutines + Swift 직접**: KMP-NativeCoroutines 사용.
|
|
- **iOS framework 가 큰**: bundle 폭발 — minimize.
|
|
- **Only 1 platform test**: 다른 platform 깨짐.
|
|
- **Big bang migration**: gradual.
|
|
|
|
## 🤖 LLM 활용 힌트
|
|
- expect / actual 가 KMP 의 핵심.
|
|
- Ktor / SQLDelight / kotlinx 가 multiplatform-ready.
|
|
- Compose Multiplatform = UI 까지 share.
|
|
- KMP-NativeCoroutines 가 Swift async/await.
|
|
|
|
## 🔗 관련 문서
|
|
- [[Mobile_KMP_Compose]]
|
|
- [[Mobile_Flutter_Patterns]]
|
|
- [[Android_Kotlin_Coroutines_Scopes]]
|