KMVI
1.2.1indexedFramework enhances application development using Model-View-Intent pattern with core components, type-safe state management, unidirectional data flow, coroutine-based asynchronicity, and extensibility.
Framework enhances application development using Model-View-Intent pattern with core components, type-safe state management, unidirectional data flow, coroutine-based asynchronicity, and extensibility.
A lightweight Kotlin Multiplatform MVI (Model-View-Intent) library built on top of AndroidX ViewModel and Coroutines.
Add the dependency to your build.gradle.kts:
# Version catalog (gradle/libs.versions.toml)
[versions]
kmvi = "<version>"
[libraries]
kmvi = { module = "io.github.natobytes:kmvi", version.ref = "kmvi" }
// build.gradle.kts
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.kmvi)
}
}
}
Or directly:
implementation("io.github.natobytes:kmvi:<version>")
KMVI supports JVM, Android, and iOS (x64, ARM64, Simulator ARM64 via XCFramework).
See the sample/ module for a complete Todo List example demonstrating collections, multiple intents, and effects (undo on remove).
KMVI implements a unidirectional data flow:
Intent -> Processor -> Flow<Result> -> ViewModel -> Action -> Reducer -> State
-> Effect -> Flow<Effect>
The Processor maps intents to a stream of results (actions and/or effects). It receives the current state so you can make decisions based on it.
class CounterProcessor : Processor<CounterIntent, CounterState> {
override fun process(
input: CounterIntent,
state: CounterState,
): Flow<Result> = when (input) {
CounterIntent.Increment -> flowOf(CounterAction.UpdateCount(state.count + ))
CounterIntent.Decrement -> flowOf(CounterAction.UpdateCount(state.count - ))
CounterIntent.Reset -> flow {
emit(CounterAction.UpdateCount())
emit(CounterEffect.CounterReset)
}
}
}
Both Processor and Reducer are fun interfaces, so you can use SAM conversion for simple cases:
val processor = Processor<CounterIntent, CounterState> { input, state ->
when (input) {
is CounterIntent.Increment -> flowOf(CounterAction.UpdateCount(state.count + 1))
is CounterIntent.Decrement -> flowOf(CounterAction.UpdateCount(state.count - 1))
is CounterIntent.Reset -> flow {
emit(CounterAction.UpdateCount(0))
emit(CounterEffect.CounterReset)
}
}
}
The Reducer is a pure function that applies an action to the current state and returns a new state.
class CounterReducer : Reducer<CounterAction, CounterState> {
override fun reduce(
action: CounterAction,
state: CounterState,
): CounterState = when (action) {
is CounterAction.UpdateCount -> state.copy(count = action.count)
}
}
Extend KMVIViewModel with your contract types and wire everything together.
class CounterViewModel : KMVIViewModel<CounterIntent, CounterAction, CounterEffect, CounterState>(
initialState = CounterState(),
processor = CounterProcessor(),
reducer = CounterReducer(),
)
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.state.collectAsState()
LaunchedEffect(Unit) {
viewModel.effects.collect { effect ->
(effect) {
CounterEffect.CounterReset -> { }
}
}
}
Column {
Text()
Button(onClick = { viewModel.process(CounterIntent.Increment) }) {
Text()
}
Button(onClick = { viewModel.process(CounterIntent.Decrement) }) {
Text()
}
Button(onClick = { viewModel.process(CounterIntent.Reset) }) {
Text()
}
}
}
Override onError in your ViewModel to handle exceptions thrown by processors. By default, errors are logged and the ViewModel continues processing subsequent intents.
class CounterViewModel : KMVIViewModel<CounterIntent, CounterAction, CounterEffect, CounterState>(
initialState = CounterState(),
processor = CounterProcessor(),
reducer = CounterReducer(),
) {
override fun onError(throwable: Throwable) {
// Log, report to crash analytics, etc.
}
}
Copyright 2026 natobytes
Licensed under the Apache License, Version 2.0
// What the user can do
sealed interface CounterIntent : Intent {
data object Increment : CounterIntent
data object Decrement : CounterIntent
data object Reset : CounterIntent
}
// Your screen state
data class CounterState(
val count: Int = 0,
) : State
// State mutations
sealed interface CounterAction : Action {
data class UpdateCount(val count: Int) : CounterAction
}
// One-shot side effects
sealed interface CounterEffect : Effect {
data object CounterReset : CounterEffect
}
| Type | Description |
|---|
Intent | Marker interface for user actions / events |
State | Marker interface for immutable UI state |
Result | Sealed interface — parent of Action and Effect |
Action | A state mutation, processed by the Reducer |
Effect | A one-shot side effect (navigation, toasts, etc.) |
Processor<I, S> | Transforms an Intent + current State into Flow<Result> |
Reducer<A, S> | Pure function: (Action, State) -> State |
KMVIViewModel<I, A, E, S> | Abstract ViewModel wiring everything together |
| Member | Type | Description |
|---|
state | StateFlow<S> | Current state, observable |
effects | Flow<E> | One-shot effects, each delivered to exactly one collector |
process(intent) | fun | Entry point — submit an intent for processing |
onError(throwable) | protected open fun | Override to handle processor errors |
Surfaced from shared tags and platforms — no rankings paid for.