206 lines
5.8 KiB
Markdown
206 lines
5.8 KiB
Markdown
# Kotlin Multiplatform (KMM) Reference
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
project/
|
|
├── shared/ # Shared KMM module
|
|
│ ├── src/
|
|
│ │ ├── commonMain/kotlin/ # Business logic, domain, data
|
|
│ │ │ ├── domain/
|
|
│ │ │ │ ├── model/
|
|
│ │ │ │ ├── repository/ # Interfaces
|
|
│ │ │ │ └── usecase/
|
|
│ │ │ ├── data/
|
|
│ │ │ │ ├── remote/ # Ktor client + DTOs
|
|
│ │ │ │ ├── local/ # SQLDelight DAOs
|
|
│ │ │ │ └── repository/ # Implementations
|
|
│ │ │ └── di/ # Koin modules
|
|
│ │ ├── androidMain/kotlin/ # Android-specific actual implementations
|
|
│ │ └── iosMain/kotlin/ # iOS-specific actual (if needed)
|
|
│ └── build.gradle.kts
|
|
├── androidApp/ # Android app module
|
|
│ ├── src/main/java/
|
|
│ │ ├── ui/ # Jetpack Compose screens
|
|
│ │ ├── presentation/ # Android ViewModels
|
|
│ │ └── di/ # Android-specific DI
|
|
│ └── build.gradle.kts
|
|
└── build.gradle.kts
|
|
```
|
|
|
|
## Shared Module: Ktor HTTP Client
|
|
|
|
```kotlin
|
|
// commonMain
|
|
expect fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient
|
|
|
|
// androidMain
|
|
actual fun httpClient(config: HttpClientConfig<*>.() -> Unit): HttpClient =
|
|
HttpClient(OkHttp) {
|
|
config(this)
|
|
engine { addInterceptor(/* logging, auth */) }
|
|
}
|
|
|
|
// Shared usage
|
|
val client = httpClient {
|
|
install(ContentNegotiation) { json() }
|
|
install(HttpTimeout) { requestTimeoutMillis = 10_000 }
|
|
defaultRequest {
|
|
url(BuildKonfig.BASE_URL)
|
|
header(HttpHeaders.ContentType, ContentType.Application.Json)
|
|
}
|
|
}
|
|
```
|
|
|
|
## SQLDelight Setup
|
|
|
|
```sql
|
|
-- ItemEntity.sq
|
|
CREATE TABLE ItemEntity (
|
|
id TEXT NOT NULL PRIMARY KEY,
|
|
title TEXT NOT NULL,
|
|
updatedAt INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
selectAll:
|
|
SELECT * FROM ItemEntity ORDER BY updatedAt DESC;
|
|
|
|
upsertItem:
|
|
INSERT OR REPLACE INTO ItemEntity (id, title, updatedAt)
|
|
VALUES (?, ?, ?);
|
|
```
|
|
|
|
```kotlin
|
|
// commonMain — Database driver expect/actual
|
|
expect class DatabaseDriverFactory {
|
|
fun createDriver(): SqlDriver
|
|
}
|
|
|
|
// androidMain
|
|
actual class DatabaseDriverFactory(private val context: Context) {
|
|
actual fun createDriver(): SqlDriver =
|
|
AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
|
|
}
|
|
```
|
|
|
|
## Shared Repository
|
|
|
|
```kotlin
|
|
// commonMain
|
|
class ItemRepositoryImpl(
|
|
private val remoteSource: ItemRemoteDataSource,
|
|
private val localSource: ItemLocalDataSource,
|
|
) : ItemRepository {
|
|
|
|
override fun observeItems(): Flow<List<Item>> =
|
|
localSource.observeAll().map { entities ->
|
|
entities.map { it.toDomain() }
|
|
}
|
|
|
|
override suspend fun refreshItems(): Result<Unit> = runCatching {
|
|
val items = remoteSource.fetchItems()
|
|
localSource.upsertAll(items.map { it.toEntity() })
|
|
}
|
|
}
|
|
```
|
|
|
|
## Android ViewModel consuming shared Flow
|
|
|
|
```kotlin
|
|
@HiltViewModel
|
|
class HomeViewModel @Inject constructor(
|
|
private val observeItems: ObserveItemsUseCase, // from shared module
|
|
private val refreshItems: RefreshItemsUseCase // from shared module
|
|
) : ViewModel() {
|
|
|
|
val uiState = observeItems()
|
|
.map { HomeUiState.Success(it) as HomeUiState }
|
|
.stateIn(
|
|
scope = viewModelScope,
|
|
started = SharingStarted.WhileSubscribed(5_000),
|
|
initialValue = HomeUiState.Loading
|
|
)
|
|
}
|
|
```
|
|
|
|
## Koin DI (Shared + Android)
|
|
|
|
```kotlin
|
|
// commonMain — shared Koin modules
|
|
val sharedModule = module {
|
|
single { DatabaseDriverFactory(get()) }
|
|
single { AppDatabase(get<DatabaseDriverFactory>().createDriver()) }
|
|
single<ItemRepository> { ItemRepositoryImpl(get(), get()) }
|
|
factory { ObserveItemsUseCase(get()) }
|
|
factory { RefreshItemsUseCase(get()) }
|
|
}
|
|
|
|
// androidApp — Android-specific module
|
|
val androidModule = module {
|
|
single<Context> { androidApplication() }
|
|
viewModel { HomeViewModel(get(), get()) }
|
|
}
|
|
|
|
// Application class
|
|
class MyApp : Application() {
|
|
override fun onCreate() {
|
|
super.onCreate()
|
|
startKoin {
|
|
androidContext(this@MyApp)
|
|
modules(sharedModule, androidModule)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Key Gradle Dependencies (shared/build.gradle.kts)
|
|
|
|
```kotlin
|
|
kotlin {
|
|
androidTarget()
|
|
// Add other targets as needed (jvm, iosArm64, etc.)
|
|
|
|
sourceSets {
|
|
commonMain.dependencies {
|
|
implementation(libs.ktor.client.core)
|
|
implementation(libs.ktor.client.content.negotiation)
|
|
implementation(libs.ktor.serialization.kotlinx.json)
|
|
implementation(libs.sqldelight.runtime)
|
|
implementation(libs.koin.core)
|
|
implementation(libs.kotlinx.coroutines.core)
|
|
implementation(libs.kotlinx.serialization.json)
|
|
}
|
|
androidMain.dependencies {
|
|
implementation(libs.ktor.client.okhttp)
|
|
implementation(libs.sqldelight.android.driver)
|
|
implementation(libs.koin.android)
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
## Compose Multiplatform (for shared UI)
|
|
|
|
Use when you want to share UI across Android + Desktop + Web:
|
|
|
|
```kotlin
|
|
// commonMain — shared composable
|
|
@Composable
|
|
fun HomeScreenContent(
|
|
state: HomeUiState,
|
|
onRetry: () -> Unit
|
|
) {
|
|
when (state) {
|
|
is HomeUiState.Loading -> CircularProgressIndicator()
|
|
is HomeUiState.Success -> ItemList(state.items)
|
|
is HomeUiState.Error -> ErrorView(state.message, onRetry)
|
|
}
|
|
}
|
|
|
|
// androidApp — wraps with Android ViewModel
|
|
@Composable
|
|
fun HomeScreen(viewModel: HomeViewModel = koinViewModel()) {
|
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
|
HomeScreenContent(state, onRetry = viewModel::refresh)
|
|
}
|
|
``` |