Install
$ agentstack add skill-felipechaux-kmp-compose-multiplatform-skill-kmp-compose-multiplatform ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Kotlin Multiplatform + Compose Multiplatform Skill
You are an expert in Kotlin Multiplatform (KMP) and Compose Multiplatform development. You follow Google's official architecture guidelines (as demonstrated in Now in Android), JetBrains Compose best practices, and the KMP community standards.
Core Principles
- Maximize shared code — write once in
commonMain, use everywhere - Clean Architecture — strict layer separation: Data → Domain → Presentation
- Feature-based modularization — organize by feature, not by layer
- Unidirectional Data Flow (UDF) — state flows down, events flow up
- Interface-first design — define contracts, inject implementations
- Platform parity — same behavior on Android and iOS unless explicitly platform-specific
Project Structure
Recommended Module Layout
root/
├── app/ # Android app entry point
├── iosApp/ # iOS app entry point (Xcode project)
├── shared/ # KMP shared module (or multi-module)
│ └── src/
│ ├── commonMain/ # Shared code for all platforms
│ ├── androidMain/ # Android-specific implementations
│ ├── iosMain/ # iOS-specific implementations
│ └── commonTest/ # Shared tests
├── build-logic/ # Convention plugins (if multi-module)
│ └── convention/ # Gradle convention plugins
└── gradle/
└── libs.versions.toml # Version catalog (ALWAYS use this)
Feature Module Layout (inside commonMain)
Each feature must follow this exact structure:
feature/
└── [feature-name]/
├── data/
│ ├── local/
│ │ ├── dao/ # Room DAOs
│ │ └── entity/ # Room entities
│ ├── remote/ # API services
│ ├── repository/ # Repository implementations
│ └── mapper/ # Data ↔ Domain mappers
├── domain/
│ ├── model/ # Domain models (pure Kotlin)
│ ├── repository/ # Repository interfaces
│ └── usecase/ # Use cases (one action per class)
├── presentation/
│ ├── ui/ # Composable screens and components
│ ├── viewmodel/ # ViewModels
│ └── state/ # UI state data classes
└── di/ # Koin module for this feature
Architecture Guidelines
Layer Responsibilities
Data Layer
- Implements repository interfaces from domain
- Maps data models to/from domain models
- Handles network requests (Ktor) and local persistence (Room/DataStore)
- Never exposes data models to domain or presentation
Domain Layer
- Pure Kotlin — NO Android/platform dependencies
- Repository interfaces (abstractions)
- Use cases: single public function
operator fun invoke() - Domain models (not database entities, not DTOs)
Presentation Layer
- ViewModels hold
StateFlow— never expose mutable state - UI State is a sealed class or data class
- Composables receive state + callbacks (no direct ViewModel access in nested composables)
- Navigation handled at screen level only
Resource/Result Pattern
Always use a sealed class for async results with typed domain errors (never raw strings):
sealed class Resource {
data class Success(val data: T) : Resource()
data class Error(val error: AppError) : Resource()
data object Loading : Resource()
}
See references/error-handling.md for the full AppError hierarchy, safeApiCall wrapper, and error-to-UI-message mapping.
Use Case Pattern
class GetUserUseCase(private val repository: UserRepository) {
suspend operator fun invoke(userId: String): Resource {
return repository.getUser(userId)
}
}
ViewModel Pattern
Use data class UiState (not sealed class) for composable state with _uiState.update { }. Expose navigation events via a separate SharedFlow:
class HomeViewModel(
private val getItemsUseCase: GetItemsUseCase
) : ViewModel() {
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow = _uiState.asStateFlow()
// One-time navigation/event channel — never put navigation in UiState
private val _events = MutableSharedFlow()
val events: SharedFlow = _events.asSharedFlow()
fun loadItems() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
getItemsUseCase()
.onSuccess { items ->
_uiState.update { it.copy(isLoading = false, items = items) }
}
.onError { error ->
_uiState.update { it.copy(isLoading = false, errorMessage = error.toUserMessage()) }
}
}
}
fun onItemClicked(id: String) {
viewModelScope.launch {
_events.emit(HomeEvent.NavigateToDetail(id))
}
}
}
// Flat data class — preferred over sealed class for composable state
data class HomeUiState(
val isLoading: Boolean = false,
val items: List = emptyList(),
val errorMessage: String? = null // human-readable, never AppError
)
// One-time events — navigation, toasts, analytics
sealed class HomeEvent {
data class NavigateToDetail(val id: String) : HomeEvent()
data object ShowUndoSnackbar : HomeEvent()
}
Collect events in the screen composable:
@Composable
fun HomeScreen(
navController: NavHostController,
viewModel: HomeViewModel = koinViewModel()
) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
// Collect one-time events
LaunchedEffect(Unit) {
viewModel.events.collect { event ->
when (event) {
is HomeEvent.NavigateToDetail -> navController.navigate(Screen.Detail.createRoute(event.id))
is HomeEvent.ShowUndoSnackbar -> { /* show snackbar */ }
}
}
}
HomeContent(uiState = uiState, onItemClick = viewModel::onItemClicked)
}
StateFlow from Repository Flow
Use stateIn() to convert a repository Flow into a ViewModel StateFlow:
val uiState: StateFlow = itemsRepository.observeItems()
.map { items -> HomeUiState(items = items) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = HomeUiState(isLoading = true)
)
Kotlin Multiplatform Patterns
Expect/Actual Pattern
Use expect/actual for platform-specific implementations:
// commonMain
expect fun getPlatformName(): String
expect class DatabaseBuilder(context: Any?) {
fun build(): AppDatabase
}
// androidMain
actual fun getPlatformName(): String = "Android"
actual class DatabaseBuilder actual constructor(private val context: Any?) {
actual fun build(): AppDatabase =
Room.databaseBuilder(context as Context, AppDatabase::class.java, "app.db").build()
}
// iosMain
actual fun getPlatformName(): String = "iOS"
actual class DatabaseBuilder actual constructor(context: Any?) {
actual fun build(): AppDatabase {
val dbFilePath = NSHomeDirectory() + "/app.db"
return Room.databaseBuilder(name = dbFilePath).build()
}
}
Source Set Configuration (build.gradle.kts)
kotlin {
androidTarget {
compilations.all {
compileTaskProvider.configure {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
}
}
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target ->
target.binaries.framework {
baseName = "shared"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
// Compose Multiplatform
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
// Navigation
implementation(libs.navigation.compose)
// Koin
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
// Ktor
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.serialization.kotlinx.json)
// Room
implementation(libs.room.runtime)
implementation(libs.room.ktx)
// DataStore
implementation(libs.datastore.preferences)
// DateTime
implementation(libs.kotlinx.datetime)
// Serialization
implementation(libs.kotlinx.serialization.json)
// Coroutines
implementation(libs.kotlinx.coroutines.core)
}
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
implementation(libs.koin.android)
implementation(libs.kotlinx.coroutines.android)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
Dependency Injection with Koin
Module Structure
// feature/home/di/HomeModule.kt
val homeModule = module {
single { HomeRepositoryImpl(get(), get()) }
factory { GetHomeDataUseCase(get()) }
viewModel { HomeViewModel(get()) }
}
Scopes — Feature-Scoped Dependencies
Use Koin scopes for dependencies that should live only as long as a feature/screen is active (e.g., a shopping cart, a multi-step form):
// Define a scope qualifier
val CartScope = named("CartScope")
val cartModule = module {
// Scoped — one instance per CartScope lifecycle
scope(CartScope) {
scoped { CartRepository(get()) }
scoped { CartViewModel(get()) }
}
}
// Open scope when entering the feature
val cartScope = getKoin().createScope("cart_session", CartScope)
val cartViewModel = cartScope.get()
// Close scope when leaving — instance is garbage collected
cartScope.close()
Lazy Injection
Use inject() (lazy delegation) instead of get() (eager) when the dependency may not be needed immediately:
class HomeViewModel : ViewModel() {
private val analyticsService: AnalyticsService by inject() // lazy
private val repository: HomeRepository = get() // eager
}
Named Qualifiers
Use named() qualifiers when you need multiple instances of the same type in the same module — a common pattern for multiple API clients or dispatchers:
val networkModule = module {
// Two HTTP clients with different base URLs, distinguished by name
single(named("main")) {
provideHttpClient(baseUrl = BuildKonfig.API_BASE_URL, tokenProvider = get())
}
single(named("auth")) {
provideHttpClient(baseUrl = BuildKonfig.AUTH_BASE_URL, tokenProvider = get())
}
// Multiple dispatchers
single(named("io")) { Dispatchers.IO }
single(named("main")) { Dispatchers.Main }
}
// Inject by name
class UserRepository(
private val mainClient: HttpClient = get(named("main")),
private val authClient: HttpClient = get(named("auth"))
)
ViewModel with SavedStateHandle
Bind SavedStateHandle in Koin using viewModelOf or the params API:
// Using viewModelOf — automatically injects SavedStateHandle
val featureModule = module {
viewModelOf(::DetailViewModel) // SavedStateHandle injected automatically
}
// Or manually via params
val featureModule = module {
viewModel { params ->
DetailViewModel(
savedStateHandle = params.get(),
getItemUseCase = get()
)
}
}
class DetailViewModel(
savedStateHandle: SavedStateHandle,
private val getItemUseCase: GetItemUseCase
) : ViewModel() {
private val itemId: String = checkNotNull(savedStateHandle[Screen.Detail.ARG_ID])
}
Central Module Aggregator
// di/AppModule.kt
fun getAllModules() = listOf(
platformModule(),
coreModule,
authModule,
homeModule,
// ... other feature modules
)
// Platform-specific (expect/actual)
expect fun platformModule(): Module
Koin Initialization
// KoinInitializer.kt
fun initKoin(appDeclaration: KoinAppDeclaration = {}) {
startKoin {
appDeclaration()
modules(getAllModules())
}
}
Android entry (Application class):
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
initKoin {
androidContext(this@MyApp)
}
}
}
iOS entry (Swift):
KoinInitializerKt.doInitKoin()
Build System
Version Catalog (gradle/libs.versions.toml)
Always use the version catalog. Never hardcode versions in build files:
[versions]
kotlin = "2.3.0"
compose-multiplatform = "1.10.1"
agp = "8.8.0"
koin = "4.1.1"
ktor = "3.0.3"
room = "2.8.4"
datastore = "1.1.1"
navigation-compose = "2.9.1"
kotlinx-coroutines = "1.10.2"
kotlinx-serialization = "1.7.3"
kotlinx-datetime = "0.6.1"
ksp = "2.3.0-1.0.32"
buildkonfig = "0.17.1"
coil = "3.0.4"
[libraries]
# Koin
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
# Ktor
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", 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
room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-ktx = { module = "androidx.room:room-ktx", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
# DataStore
datastore-preferences = { module = "androidx.datastore:datastore-preferences-core", version.ref = "datastore" }
# Navigation
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
# KotlinX
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" }
# Coil
coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil" }
coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor3", version.ref = "coil" }
[plugins]
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
android-library = { id = "com.android.library", version.ref = "agp" }
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [felipechaux](https://github.com/felipechaux)
- **Source:** [felipechaux/kmp-compose-multiplatform-skill](https://github.com/felipechaux/kmp-compose-multiplatform-skill)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.