AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
SKILL verified MIT Self-run

Kmp Compose Multiplatform

skill-felipechaux-kmp-compose-multiplatform-skill-kmp-compose-multiplatform · by felipechaux

Expert Kotlin Multiplatform (KMP) and Compose Multiplatform development guidance. Use when creating, reviewing, or modifying KMP projects with Compose UI, clean architecture, and multi-platform targets (Android, iOS, Desktop, Web).

No reviews yet
0 installs
35 views
0.0% view→install

Install

$ agentstack add skill-felipechaux-kmp-compose-multiplatform-skill-kmp-compose-multiplatform

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/skill-felipechaux-kmp-compose-multiplatform-skill-kmp-compose-multiplatform)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
6mo ago

Declared compatibility

Claude CodeClaude Desktop

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

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 →
Are you the author of Kmp Compose Multiplatform? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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

  1. Maximize shared code — write once in commonMain, use everywhere
  2. Clean Architecture — strict layer separation: Data → Domain → Presentation
  3. Feature-based modularization — organize by feature, not by layer
  4. Unidirectional Data Flow (UDF) — state flows down, events flow up
  5. Interface-first design — define contracts, inject implementations
  6. 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.

Versions

  • v0.1.0 Imported from the upstream source.