[G1-Sync] Manual knowledge update
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
---
|
||||
id: mobile-kmp-compose
|
||||
title: Kotlin Multiplatform / Compose Multiplatform
|
||||
category: Coding
|
||||
status: draft
|
||||
source_trust_level: B
|
||||
verification_status: conceptual
|
||||
created_at: 2026-05-09
|
||||
updated_at: 2026-05-09
|
||||
tags: [mobile, kotlin, kmp, compose, vibe-coding]
|
||||
tech_stack: { language: "Kotlin", applicable_to: ["iOS", "Android", "Desktop", "Web"] }
|
||||
applied_in: []
|
||||
aliases: [KMP, Kotlin Multiplatform, Compose Multiplatform, expect/actual, KMM]
|
||||
---
|
||||
|
||||
# Kotlin Multiplatform (KMP) + Compose Multiplatform
|
||||
|
||||
> Android / iOS / Desktop / Web 같은 비즈니스 로직 (KMP). UI 도 공유 가능 (Compose Multiplatform). **Native UI 가 strict 면 KMP 만, fast share 면 둘 다**.
|
||||
|
||||
## 📖 핵심 개념
|
||||
- KMP: 비즈니스 로직 (data, network, repository) 공유.
|
||||
- Compose Multiplatform: UI 도 같은 코드.
|
||||
- expect/actual: platform-specific.
|
||||
- iOS = framework / Cocoapods 으로 import.
|
||||
|
||||
## 💻 코드 패턴
|
||||
|
||||
### 폴더 (gradle)
|
||||
```
|
||||
shared/
|
||||
src/
|
||||
commonMain/ # 모든 platform
|
||||
androidMain/ # Android only
|
||||
iosMain/ # iOS only
|
||||
desktopMain/ # JVM desktop
|
||||
jsMain/ # Web
|
||||
androidApp/
|
||||
iosApp/
|
||||
desktopApp/
|
||||
```
|
||||
|
||||
### shared module (build.gradle.kts)
|
||||
```kotlin
|
||||
plugins {
|
||||
kotlin("multiplatform")
|
||||
id("com.android.library")
|
||||
}
|
||||
|
||||
kotlin {
|
||||
androidTarget()
|
||||
iosX64()
|
||||
iosArm64()
|
||||
iosSimulatorArm64()
|
||||
jvm("desktop")
|
||||
|
||||
cocoapods {
|
||||
version = "1.0"
|
||||
summary = "Shared module"
|
||||
homepage = "..."
|
||||
ios.deploymentTarget = "17.0"
|
||||
framework {
|
||||
baseName = "Shared"
|
||||
isStatic = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation("io.ktor:ktor-client-core:2.3.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0")
|
||||
}
|
||||
androidMain.dependencies {
|
||||
implementation("io.ktor:ktor-client-okhttp:2.3.0")
|
||||
}
|
||||
iosMain.dependencies {
|
||||
implementation("io.ktor:ktor-client-darwin:2.3.0")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Common code
|
||||
```kotlin
|
||||
// shared/commonMain/Repository.kt
|
||||
class UserRepository(private val client: HttpClient) {
|
||||
suspend fun fetchUser(id: String): User {
|
||||
return client.get("$BASE/users/$id").body()
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class User(val id: String, val email: String, val name: String)
|
||||
```
|
||||
|
||||
### expect/actual (platform 별 구현)
|
||||
```kotlin
|
||||
// commonMain
|
||||
expect class Platform() {
|
||||
val name: String
|
||||
}
|
||||
|
||||
// androidMain
|
||||
actual class Platform actual constructor() {
|
||||
actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"
|
||||
}
|
||||
|
||||
// iosMain
|
||||
import platform.UIKit.UIDevice
|
||||
actual class Platform actual constructor() {
|
||||
actual val name: String = "iOS ${UIDevice.currentDevice.systemVersion}"
|
||||
}
|
||||
```
|
||||
|
||||
### Android 사용
|
||||
```kotlin
|
||||
// androidApp
|
||||
val repo = UserRepository(httpClient)
|
||||
val user = lifecycleScope.launch { repo.fetchUser("u1") }
|
||||
```
|
||||
|
||||
### iOS 사용 — Swift
|
||||
```swift
|
||||
import Shared
|
||||
|
||||
let repo = UserRepository(client: ...)
|
||||
Task {
|
||||
let user = try await repo.fetchUser(id: "u1")
|
||||
}
|
||||
|
||||
// Kotlin suspend → Swift async/await 자동 (KMP 1.9+)
|
||||
```
|
||||
|
||||
### Compose Multiplatform (UI 공유)
|
||||
```kotlin
|
||||
// shared/commonMain/App.kt
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.material3.*
|
||||
|
||||
@Composable
|
||||
fun App(repo: UserRepository) {
|
||||
var user by remember { mutableStateOf<User?>(null) }
|
||||
LaunchedEffect(Unit) { user = repo.fetchUser("u1") }
|
||||
|
||||
Scaffold { padding ->
|
||||
Text(user?.name ?? "Loading", modifier = Modifier.padding(padding))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```swift
|
||||
// iosApp/iOSApp.swift
|
||||
import SwiftUI
|
||||
import Shared
|
||||
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
ComposeView()
|
||||
.ignoresSafeArea(.all, edges: .bottom)
|
||||
}
|
||||
}
|
||||
|
||||
struct ComposeView: UIViewControllerRepresentable {
|
||||
func makeUIViewController(context: Context) -> UIViewController {
|
||||
return Main_iosKt.MainViewController()
|
||||
}
|
||||
func updateUIViewController(_ vc: UIViewController, context: Context) {}
|
||||
}
|
||||
```
|
||||
|
||||
### Network — Ktor Client
|
||||
```kotlin
|
||||
val client = HttpClient {
|
||||
install(ContentNegotiation) {
|
||||
json(Json { ignoreUnknownKeys = true })
|
||||
}
|
||||
defaultRequest {
|
||||
url(BASE_URL)
|
||||
header("Authorization", "Bearer $token")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Database — SQLDelight
|
||||
```kotlin
|
||||
// shared/commonMain/sqldelight/com/acme/AppDatabase.sq
|
||||
CREATE TABLE User (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
email TEXT NOT NULL
|
||||
);
|
||||
|
||||
selectAll:
|
||||
SELECT * FROM User;
|
||||
|
||||
insert:
|
||||
INSERT OR REPLACE INTO User VALUES (?, ?);
|
||||
```
|
||||
|
||||
```kotlin
|
||||
val db = AppDatabase(driver)
|
||||
db.userQueries.insert("u1", "a@b.com")
|
||||
val users = db.userQueries.selectAll().executeAsList()
|
||||
```
|
||||
|
||||
### State management
|
||||
```kotlin
|
||||
class UserViewModel(private val repo: UserRepository) {
|
||||
private val _state = MutableStateFlow<UiState>(UiState.Loading)
|
||||
val state: StateFlow<UiState> = _state
|
||||
|
||||
suspend fun load(id: String) {
|
||||
_state.value = UiState.Loading
|
||||
try {
|
||||
_state.value = UiState.Success(repo.fetchUser(id))
|
||||
} catch (e: Exception) {
|
||||
_state.value = UiState.Error(e.message ?: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class UiState {
|
||||
object Loading : UiState()
|
||||
data class Success(val user: User) : UiState()
|
||||
data class Error(val msg: String) : UiState()
|
||||
}
|
||||
```
|
||||
|
||||
### iOS 호출 — coroutines 변환
|
||||
```
|
||||
// KMP 1.9+ 자동 async/await.
|
||||
// 또는 NativeCoroutines 라이브러리 — Combine / async stream.
|
||||
```
|
||||
|
||||
### Test (commonTest)
|
||||
```kotlin
|
||||
class UserRepositoryTest {
|
||||
@Test
|
||||
fun fetchUser() = runTest {
|
||||
val mock = MockEngine { ... }
|
||||
val repo = UserRepository(HttpClient(mock))
|
||||
val user = repo.fetchUser("u1")
|
||||
assertEquals("u1", user.id)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
→ JVM 에서 실행 (commonTest), iOS / Android target 에 자동 적용.
|
||||
|
||||
### Trade-offs
|
||||
```
|
||||
KMP 비즈니스 로직만:
|
||||
+ Native UI 강력 / 잘 쓰면 빠름
|
||||
+ iOS / Android 가 Swift / Kotlin 그대로
|
||||
- UI 코드 두 번
|
||||
|
||||
Compose Multiplatform:
|
||||
+ UI 도 한 코드
|
||||
- iOS UI 가 Material — Apple Human Interface 와 차이
|
||||
- 일부 native 기능 어려움 (camera, notification)
|
||||
```
|
||||
|
||||
## 🤔 의사결정 기준
|
||||
| 상황 | 추천 |
|
||||
|---|---|
|
||||
| Logic 만 공유 | KMP only |
|
||||
| Logic + UI 같이 | Compose Multiplatform |
|
||||
| Native UX 강 critical | KMP only |
|
||||
| Internal tool / B2B | Compose MP |
|
||||
| Consumer app | KMP + native UI |
|
||||
| Cross-platform alternative | React Native / Flutter |
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **Compose iOS = Material design 그대로**: HIG 위반. 별 styling.
|
||||
- **Common code 안 platform 의존**: 빌드 깨짐. expect/actual.
|
||||
- **iOS 의 Combine / SwiftUI 통합 무시**: NativeCoroutines.
|
||||
- **Cocoapods + SPM 혼합**: 한 가지 선택.
|
||||
- **빌드 시간 무시**: KMP 가 처음 빌드 길음.
|
||||
- **Native API + KMP 의존**: 양쪽 의존. Either / Or.
|
||||
- **Web target 가정 prod ready**: Compose Web 은 아직 alpha.
|
||||
|
||||
## 🤖 LLM 활용 힌트
|
||||
- 비즈니스 로직 = KMP, UI = native (대부분 case).
|
||||
- Ktor + SQLDelight + kotlinx-serialization 3종 표준.
|
||||
- expect/actual = platform-specific.
|
||||
|
||||
## 🔗 관련 문서
|
||||
- [[Android_Compose_State_Hoisting]]
|
||||
- [[iOS_SwiftUI_State_Property_Wrappers]]
|
||||
- [[React_Native_Bridge_Performance]]
|
||||
Reference in New Issue
Block a user