$npx -y skills add mmiani/kotlin-kmp-claude-agent-skills --skill kotlin-project-state-managementUse when choosing, implementing, or reviewing state-holder patterns in a KMP project — ViewModel, shared presenter, MVI, or StateFlow-in-common — including effect handling, UiState modeling, and testability.
| 1 | # Kotlin Multiplatform State Management |
| 2 | |
| 3 | Use this skill when choosing, implementing, or reviewing how UI state is owned and produced in a Kotlin Multiplatform project. |
| 4 | |
| 5 | State management sits at the intersection of every other KMP architectural concern. How state is held, where it lives in source sets, and how it integrates with each platform's lifecycle determines the testability, predictability, and long-term maintainability of the entire UI layer. |
| 6 | |
| 7 | This skill is intentionally precise. It distinguishes between patterns clearly, explains when each is appropriate, and flags common mistakes that look correct at first but create problems at scale. |
| 8 | |
| 9 | ## What this skill covers |
| 10 | |
| 11 | - The invariant state contract that all valid state-holder patterns must satisfy |
| 12 | - The main pattern families available in KMP and what each costs |
| 13 | - Platform-specific differences that affect state-holder choice |
| 14 | - Effect handling: one-time events vs persistent state |
| 15 | - State modeling: how to shape `UiState` correctly |
| 16 | - Testability implications of each choice |
| 17 | - Anti-patterns in each category |
| 18 | |
| 19 | ## What this skill does not cover |
| 20 | |
| 21 | - Navigation (see `kotlin-navigation-compose-multiplatform`) |
| 22 | - Data-layer design (see `kotlin-data-kmp-data-layer`) |
| 23 | - Full architecture review (see `kotlin-project-architecture-review`) |
| 24 | - Full feature implementation (see `kotlin-project-feature-implementation`) |
| 25 | |
| 26 | --- |
| 27 | |
| 28 | ## The state-holder contract |
| 29 | |
| 30 | Regardless of which pattern a project uses, a valid state holder must satisfy this contract: |
| 31 | |
| 32 | 1. **One observable state output** — a single stream of immutable `UiState` that the UI renders from. Not several scattered booleans, not a mutable object the UI reads fields from directly. |
| 33 | 2. **Separate one-time effects** — navigation triggers, snackbar requests, permission launches, and similar one-shot actions must not be modeled as persistent state. Once consumed, they must not replay. |
| 34 | 3. **User actions as inputs** — the UI sends events or calls action functions; it does not set state directly or coordinate work. |
| 35 | 4. **No business rules in rendering** — the state holder processes inputs, coordinates lower layers, and produces output. Composables render. |
| 36 | 5. **Platform lifecycle transparency** — the state holder must behave correctly across the platform lifecycle events relevant to its target (configuration changes on Android, view disappear/appear on iOS, window focus on desktop). |
| 37 | |
| 38 | Every pattern described below is evaluated against this contract. |
| 39 | |
| 40 | --- |
| 41 | |
| 42 | ## Pattern families |
| 43 | |
| 44 | ### 1. Android ViewModel with StateFlow (Android-only or Android-primary) |
| 45 | |
| 46 | **What it is:** |
| 47 | `ViewModel` from AndroidX lifecycle, holding a `MutableStateFlow<UiState>` privately and exposing it as `StateFlow<UiState>`. Effects emitted via a separate `SharedFlow` or `Channel`-backed flow. |
| 48 | |
| 49 | **When it is the right choice:** |
| 50 | - The project targets Android only, or Android plus other platforms where ViewModel wrapper libraries are used |
| 51 | - The team is already fluent with ViewModel semantics |
| 52 | - Jetpack Compose with `collectAsStateWithLifecycle()` or `collectAsState()` is the rendering layer |
| 53 | |
| 54 | **What it provides:** |
| 55 | - Automatic scoping to Android's ViewModel lifecycle (survives configuration changes by default via `viewModelScope`) |
| 56 | - Strong Jetpack integration (`hiltViewModel()`, Navigation ViewModel scoping, `SavedStateHandle`) |
| 57 | - Well-understood by Android developers |
| 58 | |
| 59 | **Platform constraint:** |
| 60 | ViewModel is an AndroidX library. It does not exist natively on iOS, desktop, or web. Projects using ViewModel in shared `commonMain` code require an additional library to provide a ViewModel-compatible abstraction on non-Android targets (see KMP ViewModel libraries below). |
| 61 | |
| 62 | **Source-set placement:** |
| 63 | - Pure Android ViewModel implementations belong in `androidMain` |
| 64 | - If the project uses a KMP ViewModel abstraction, the shared interface can live in `commonMain` |
| 65 | |
| 66 | **Testability:** |
| 67 | - Good: can be tested on JVM with `kotlinx-coroutines-test`, `TestScope`, `runTest`, and `Turbine` or `collectValues()` |
| 68 | - `viewModelScope` must be replaced with an injected `CoroutineScope` in tests, or use the `ViewModelScenario`/rule pattern from `lifecycle-viewmodel-testing` |
| 69 | |
| 70 | --- |
| 71 | |
| 72 | ### 2. Shared presenter / state machine in commonMain |
| 73 | |
| 74 | **What it is:** |
| 75 | A plain Kotlin class in `commonMain` that holds a `MutableStateFlow<UiState>` and exposes it, processes user actions via functions or a sealed `Action` type, and emits effects. No AndroidX dependency. Lifecycle management is the platform entry point's responsibility. |
| 76 | |
| 77 | **When it is the right choice:** |
| 78 | - The project targets multiple platforms and wants state-holder logic to be shared and tested once |
| 79 | - The team wants to avoid a depend |