kmp-mvi-setup — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited kmp-mvi-setup (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
This skill guides you through creating a production-ready Kotlin Multiplatform (KMP) project using MVI (Model-View-Intent) architecture with Compose Multiplatform.
IMPORTANT: Before writing any code or project structure, you MUST ask the user all Pre-flight Questions below and wait for answers. Never assume defaults silently.
PREREQUISITE SKILLS: Read these skills first before generating any files: -kmp-versions— always fetch live compatible versions from official sources -kmp-project-structure— use the new multi-module layout (shared/+androidApp/)
Ask the user these questions in a single message before generating anything. Wait for all answers before reading any reference files or writing any code.
Before I scaffold your KMP project, I need a few details:
1. ℹ️ App name & package
What is your app name and base package?
e.g. App Name: "Task Manager", Package: "com.example.taskmanager"
2. 📱 Target platforms
- Android only
- Android + Desktop (JVM)
- Android + Desktop + iOS (requires macOS for iOS build)
3. 🔧 Dependency Injection
- Koin (recommended — KMP-native, simple DSL, no code gen)
- Manual DI (constructor injection, no library)
- Other — please specify
4. 🌐 Networking
Do you need HTTP API calls?
- Ktor (recommended — KMP-native, multiplatform engines)
- None (no networking needed)
- Other — please specify
5. 📦 Serialization
Do you need JSON or data encoding?
- kotlinx.serialization (recommended — works on all targets)
- None (no serialization needed)
- Other — please specify
6. 🖼️ Image loading
Do you need to load images from URLs?
- Coil 3 + Landscapist (recommended — KMP-ready, composable wrappers)
- Coil 3 standalone (ImageRequest API directly)
- None (skip for now)
- Other — please specify
7. 🦭 Navigation
Do you want type-safe navigation-compose scaffolded?
- Yes (recommended — @Serializable routes, NavHost, NavController via Effect)
- No (I’ll add it later)
8. 🗄️ Local Storage / Database
Do you need persistent local data storage?
- multiplatform-settings (small data: prefs, flags, settings — KMP-native)
- SQLDelight (recommended for structured data — full KMP: Android + iOS + Desktop)
- Room (structured data — Android/JVM only, no iOS)
- Both multiplatform-settings + SQLDelight (prefs + structured data)
- None (no local storage needed)After the user answers the pre-flight questions, read only the reference files that match their choices. Do not load references for modules the user did not select.
| User selects | Reference file to read | When to skip |
|---|---|---|
| Any (always) | references/mvi-patterns.md | Never — always read this |
| Koin | references/di-options.md | User chose Manual DI |
| Ktor | references/network-ktor.md | User chose None |
| kotlinx.serialization | references/serialization.md | User chose None |
| Coil / Landscapist | references/image-loading.md | User chose None |
| Navigation | references/navigation.md | User chose No |
| multiplatform-settings | references/storage-datastore.md | User chose None |
| SQLDelight | references/storage-sqldelight.md | User chose None |
| Room | references/storage-room.md | User chose None |
| Both settings + SQLDelight | Both storage-datastore.md + storage-sqldelight.md | — |
Rule: Only include libraries, Gradle entries, and code from the selected reference files. Never add a dependency the user did not ask for.
If the user requests a library not covered by any reference file in this skill (e.g. WorkManager, Firebase, Apollo GraphQL, Coil-SVG, Bluetooth, etc.), the AI should:
FETCH_FROM_OFFICIAL_SOURCE placeholderscommonMainexpect/actual for platform-specific code if neededreferences/<module-name>.md so it can be reused in future projects.This keeps the skill growing organically without requiring manual updates for every possible library.
Use the answers to generate:
kmp-project-structure skill)settings.gradle.ktsbuild.gradle.ktsshared/build.gradle.kts — only selected dependenciesandroidApp/build.gradle.ktsdesktopApp/build.gradle.kts (if Desktop selected)gradle/libs.versions.toml — only selected libraries, fetched versions (see below)UiState.kt, UiEvent.kt, UiEffect.kt, BaseViewModel.ktAppNavigation.kt — if navigation selectedMainActivity.kt, main.kt (Desktop), App.kt (shared)Use the new multi-module layout (required for AGP 9.0+). See kmp-project-structure for full file contents.
<AppName>/
├── shared/ ← KMP library: ALL shared code + shared UI
│ ├── build.gradle.kts ← com.android.kotlin.multiplatform.library + kotlin.multiplatform
│ └── src/
│ ├── commonMain/kotlin/<package>/
│ │ ├── App.kt ← Root @Composable (shared)
│ │ ├── mvi/
│ │ │ ├── UiState.kt
│ │ │ ├── UiEvent.kt
│ │ │ ├── UiEffect.kt
│ │ │ └── BaseViewModel.kt
│ │ ├── di/
│ │ │ └── AppModule.kt ← Shared DI module
│ │ └── navigation/
│ │ └── AppNavigation.kt ← NavHost + routes (if selected)
│ ├── androidMain/kotlin/<package>/
│ │ └── di/
│ │ └── AndroidModule.kt
│ └── jvmMain/kotlin/<package>/ ← only if Desktop selected
│ └── di/
│ └── DesktopModule.kt
│
├── androidApp/ ← Thin Android shell (entry point only)
│ ├── build.gradle.kts ← com.android.application only
│ └── src/main/kotlin/<package>/
│ ├── MainActivity.kt ← setContent { App() }
│ └── MyApplication.kt ← DI init (if Koin)
│
├── desktopApp/ ← only if Desktop selected
│ ├── build.gradle.kts ← kotlin("jvm")
│ └── src/main/kotlin/
│ └── main.kt ← application { Window { App() } }
│
├── iosApp/ ← Xcode project (if iOS selected)
├── gradle/
│ └── libs.versions.toml
├── build.gradle.kts
├── settings.gradle.kts
├── gradle.properties
└── .gitignoreSee references/mvi-patterns.md for the complete pattern.
Every feature in the app follows this contract:
// State — immutable snapshot of what the UI renders
data class FeatureUiState(
val isLoading: Boolean = false,
val data: List<ItemModel> = emptyList(),
val error: String? = null
)
// Event — all user actions / one-directional: UI → ViewModel
sealed interface FeatureUiEvent {
data class LoadData(val id: String) : FeatureUiEvent
data object RetryClicked : FeatureUiEvent
data object DismissError : FeatureUiEvent
}
// Effect — one-shot side effects, not stored in State
sealed interface FeatureUiEffect {
data class NavigateTo(val route: String) : FeatureUiEffect
data class ShowSnackbar(val message: String) : FeatureUiEffect
}// commonMain — shared ViewModel base, no Android dependency
abstract class BaseViewModel<State, Event, Effect> {
private val initialState: State by lazy { createInitialState() }
protected val _state: MutableStateFlow<State> = MutableStateFlow(initialState)
val state: StateFlow<State> = _state.asStateFlow()
private val _effect = MutableSharedFlow<Effect>(extraBufferCapacity = 16)
val effect: SharedFlow<Effect> = _effect.asSharedFlow()
protected val viewModelScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
abstract fun createInitialState(): State
abstract fun onEvent(event: Event)
protected fun setState(reduce: State.() -> State) {
_state.update { it.reduce() }
}
protected fun sendEffect(effect: Effect) {
viewModelScope.launch { _effect.emit(effect) }
}
open fun onCleared() {
viewModelScope.cancel()
}
}See references/di-options.md for full setup per framework.
// commonMain — sharedModule
val sharedModule = module {
// Add your shared dependencies here
}
// androidMain — Android Application.onCreate()
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApplication)
modules(sharedModule, androidModule)
}
}
}
// jvmMain — main()
fun main() = application {
startKoin { modules(sharedModule, desktopModule) }
Window(onCloseRequest = ::exitApplication, title = "AppName") {
App()
}
}See references/navigation.md for full setup.
// commonMain — type-safe routes using @Serializable
@Serializable object HomeRoute
@Serializable data class DetailRoute(val id: String)
// NavHost setup
@Composable
fun AppNavigation(navController: NavHostController = rememberNavController()) {
NavHost(navController = navController, startDestination = HomeRoute) {
composable<HomeRoute> {
HomeScreen(onNavigateToDetail = { id ->
navController.navigate(DetailRoute(id))
})
}
composable<DetailRoute> { backStackEntry ->
val route: DetailRoute = backStackEntry.toRoute()
DetailScreen(id = route.id)
}
}
}See references/image-loading.md for full setup.
⚠️ Never hardcode versions. Always follow the 5-step protocol inkmp-versionsto fetch live compatible versions before filling inlibs.versions.toml. The table below shows which sources to check per library:
| Library | Fetch version from |
|---|---|
| Kotlin / KGP | https://kotlinlang.org/docs/multiplatform/multiplatform-compatibility-guide.html |
| AGP | https://developer.android.com/build/releases/gradle-plugin |
| Compose Multiplatform | https://plugins.gradle.org/plugin/org.jetbrains.compose |
| kotlinx-coroutines | https://github.com/Kotlin/kotlinx.coroutines/releases |
| kotlinx-serialization | https://github.com/Kotlin/kotlinx.serialization/releases |
| Ktor | https://github.com/ktorio/ktor/releases |
| Koin | https://github.com/InsertKoinIO/koin/releases |
| Lifecycle / ViewModel | https://developer.android.com/jetpack/androidx/releases/lifecycle |
| Navigation Compose | https://developer.android.com/jetpack/androidx/releases/navigation |
| Coil 3 | https://github.com/coil-kt/coil/releases |
| Landscapist | https://github.com/skydoves/landscapist/releases |
[versions]
# ── Fetch from official sources (see kmp-versions skill) ──────────────────────
kotlin = "FETCH"
agp = "FETCH"
composeMultiplatform = "FETCH"
coroutines = "FETCH"
lifecycle = "FETCH"
[libraries]
# Compose Multiplatform — explicit (compose.* accessors deprecated in CMP 1.10)
compose-runtime = { module = "org.jetbrains.compose.runtime:runtime", version.ref = "composeMultiplatform" }
compose-foundation = { module = "org.jetbrains.compose.foundation:foundation", version.ref = "composeMultiplatform" }
compose-ui = { module = "org.jetbrains.compose.ui:ui", version.ref = "composeMultiplatform" }
compose-material3 = { module = "org.jetbrains.compose.material3:material3", version.ref = "composeMultiplatform" }
# material-icons-core is NOT transitive since CMP 1.8.2 — always declare explicitly
compose-material-icons-core = { module = "org.jetbrains.compose.material:material-icons-core", version.ref = "composeMultiplatform" }
compose-ui-tooling-preview = { module = "org.jetbrains.compose.ui:ui-tooling-preview", version.ref = "composeMultiplatform" }
# Kotlin
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
lifecycle-viewmodel = { module = "androidx.lifecycle:lifecycle-viewmodel", version.ref = "lifecycle" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
# Android entry point
androidx-activity-compose = { module = "androidx.activity:activity-compose", version = "FETCH" }
[plugins]
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
androidApplication = { id = "com.android.application", version.ref = "agp" }
# AGP 9+ new plugin for the shared module
androidKmpLibrary = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" }
composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }Each module's reference file contains its exact[versions]and[libraries]entries. Copy them intolibs.versions.tomlif the user selected that module.
| Module | Reference file with entries |
|---|---|
| kotlinx.serialization | references/serialization.md — [versions] + [libraries] + [plugins] |
| Ktor | references/network-ktor.md — [versions] + [libraries] |
| Koin | references/di-options.md — [versions] + [libraries] |
| Navigation Compose | references/navigation.md — [versions] + [libraries] |
| Coil + Landscapist | references/image-loading.md — [versions] + [libraries] |
commonMain — never in androidMain or jvmMain.androidMain and jvmMain contain only: entry points, platform-specific DI modules, and platform actual implementations.interface + DI for platform differences — avoid expect class (Beta warning).BaseViewModel defined in commonMain.data class for UiState, sealed interface for UiEvent and UiEffect.@Serializable data classes/objects (Kotlin 2.x type-safe API).Strings object or resource file.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.