
MVI framework offering distributed reducers and composable features: type-safe sealed intentions, pure outcome reducers, DI-pluggable feature sets, and compile-time code generation for zero boilerplate.
A Kotlin-first MVI (Model-View-Intent) framework for Android & Kotlin Multiplatform with compile-time code generation via KSP.
@AutoState + @AutoFeature generate the retained store and Hilt/Koin modulesMost state management approaches — whether MVVM ViewModels or MVI frameworks with central reducers — tend toward the same problem: logic accumulates in one place. The ViewModel becomes a god object, or the reducer becomes a god function with dozens of cases.
YAMV takes a different approach: there is no central reducer.
In a typical MVI framework, a single reduce() function handles every action:
// Typical MVI — one reducer grows with every action
fun reduce(state: S, action: Action): S = when (action) {
is Increment -> state.copy(count = state.count + 1)
is SetLoading -> state.copy(loading = true)
is SetData -> state.copy(data = action.data)
// ... grows linearly
}In YAMV, each state transformation is its own StateOutcome class — a pure (S) -> S function:
class IncrementOutcome : StateOutcome<CounterState> {
override fun reduce(prevState: CounterState) =
prevState.copy(count = prevState.count + 1)
}Testing is trivial — no coroutines, no Flow, no store setup:
@Test fun `increment adds one`() {
val result = IncrementOutcome().reduce(CounterState(count = 5))
assertThat(result.count).isEqualTo(6)
}A Feature decides when and which outcome to emit (the orchestration). An Outcome decides how state changes (the transformation). Two independent concerns, two independent test targets.
Each feature handles one intention type. Each outcome handles one state transition. Adding new behavior means adding a new file — not modifying an existing ViewModel or reducer. Features are injected as a Set<Feature<S>>, so they are truly pluggable: add or remove a feature from the DI set without touching any other code.
Set<Feature<S>> injection means features can be conditionally included via DI configuration — A/B tests, feature flags, build variants — without touching any code. Swap, add, or remove behaviors entirely at the wiring level.
Features run on Dispatchers.Default (concurrent), reducers apply on Dispatchers.Main (serialized) — correct by default. No manual dispatcher management per method. Per-feature and per-state dispatcher customization is available when needed.
// build.gradle.kts (app module)
dependencies {
// Core framework
implementation("io.github.ktomek:yamv:VERSION")
// Android ViewModel integration
implementation("io.github.ktomek:yamv-retainer:VERSION")
// Choose your DI integration:
implementation("io.github.ktomek:yamv-hilt:VERSION") // Hilt
// or
implementation("io.github.ktomek:yamv-koin:VERSION") // Koin (multiplatform)
// KSP code generation (Hilt only)
ksp("io.github.ktomek:yamv-processor-hilt:VERSION")
}Replace VERSION with the latest badge version above.
@AutoState
data class CounterState(val count: Int = 0) : State
sealed class CounterIntention {
data object Increment : CounterIntention()
data object Decrement : CounterIntention()
}Declare reducers as separate classes — decoupled from features, independently testable:
// Outcome — pure (S) -> S, tested without coroutines
class IncrementOutcome : StateOutcome<CounterState> {
override fun reduce(prevState: CounterState) =
prevState.copy(count = prevState.count + 1)
}
// Feature — maps intentions to outcomes
@AutoFeature
class IncrementFeature : TypedFeature<CounterState, CounterIntention.Increment> {
override fun invoke(intentions: Flow<CounterIntention.Increment>): Flow<Outcome<CounterState>> =
intentions.map { IncrementOutcome() }
}@Composable
fun CounterScreen(store: CounterStateStore = hiltMviStore()) {
val state by store.state.collectAsStateWithLifecycle()
Column {
Text("Count: ${state.count}")
Button(onClick = { store.dispatch(CounterIntention.Increment) }) {
Text("+")
}
}
}That's it. @AutoState generates CounterStateStore and @AutoFeature generates the Hilt bindings.
| Module | Description | Platform |
|---|---|---|
yamv-core |
Marker interfaces & annotations (State, Outcome, @AutoState, @AutoFeature) |
Pure Kotlin |
yamv |
Core runtime (MviRuntime, MviStore, FeatureRouter) |
Kotlin Multiplatform |
yamv-retainer |
MviRetainedStore — lifecycle-retained ViewModel base |
Android + iOS |
yamv-hilt |
hiltMviStore() Compose helper |
Android |
yamv-koin |
koinMviStore() / mviStore {} DSL |
Kotlin Multiplatform |
yamv-processor-core |
KSP utilities (DI-agnostic) | JVM |
yamv-processor-hilt |
Hilt KSP processor — generates *Store + *FeaturesModule
|
JVM |
Full documentation: ktomek.github.io/yamv
Copyright 2024 Tomasz Kaszkowiak
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
A Kotlin-first MVI (Model-View-Intent) framework for Android & Kotlin Multiplatform with compile-time code generation via KSP.
@AutoState + @AutoFeature generate the retained store and Hilt/Koin modulesMost state management approaches — whether MVVM ViewModels or MVI frameworks with central reducers — tend toward the same problem: logic accumulates in one place. The ViewModel becomes a god object, or the reducer becomes a god function with dozens of cases.
YAMV takes a different approach: there is no central reducer.
In a typical MVI framework, a single reduce() function handles every action:
// Typical MVI — one reducer grows with every action
fun reduce(state: S, action: Action): S = when (action) {
is Increment -> state.copy(count = state.count + 1)
is SetLoading -> state.copy(loading = true)
is SetData -> state.copy(data = action.data)
// ... grows linearly
}In YAMV, each state transformation is its own StateOutcome class — a pure (S) -> S function:
class IncrementOutcome : StateOutcome<CounterState> {
override fun reduce(prevState: CounterState) =
prevState.copy(count = prevState.count + 1)
}Testing is trivial — no coroutines, no Flow, no store setup:
@Test fun `increment adds one`() {
val result = IncrementOutcome().reduce(CounterState(count = 5))
assertThat(result.count).isEqualTo(6)
}A Feature decides when and which outcome to emit (the orchestration). An Outcome decides how state changes (the transformation). Two independent concerns, two independent test targets.
Each feature handles one intention type. Each outcome handles one state transition. Adding new behavior means adding a new file — not modifying an existing ViewModel or reducer. Features are injected as a Set<Feature<S>>, so they are truly pluggable: add or remove a feature from the DI set without touching any other code.
Set<Feature<S>> injection means features can be conditionally included via DI configuration — A/B tests, feature flags, build variants — without touching any code. Swap, add, or remove behaviors entirely at the wiring level.
Features run on Dispatchers.Default (concurrent), reducers apply on Dispatchers.Main (serialized) — correct by default. No manual dispatcher management per method. Per-feature and per-state dispatcher customization is available when needed.
// build.gradle.kts (app module)
dependencies {
// Core framework
implementation("io.github.ktomek:yamv:VERSION")
// Android ViewModel integration
implementation("io.github.ktomek:yamv-retainer:VERSION")
// Choose your DI integration:
implementation("io.github.ktomek:yamv-hilt:VERSION") // Hilt
// or
implementation("io.github.ktomek:yamv-koin:VERSION") // Koin (multiplatform)
// KSP code generation (Hilt only)
ksp("io.github.ktomek:yamv-processor-hilt:VERSION")
}Replace VERSION with the latest badge version above.
@AutoState
data class CounterState(val count: Int = 0) : State
sealed class CounterIntention {
data object Increment : CounterIntention()
data object Decrement : CounterIntention()
}Declare reducers as separate classes — decoupled from features, independently testable:
// Outcome — pure (S) -> S, tested without coroutines
class IncrementOutcome : StateOutcome<CounterState> {
override fun reduce(prevState: CounterState) =
prevState.copy(count = prevState.count + 1)
}
// Feature — maps intentions to outcomes
@AutoFeature
class IncrementFeature : TypedFeature<CounterState, CounterIntention.Increment> {
override fun invoke(intentions: Flow<CounterIntention.Increment>): Flow<Outcome<CounterState>> =
intentions.map { IncrementOutcome() }
}@Composable
fun CounterScreen(store: CounterStateStore = hiltMviStore()) {
val state by store.state.collectAsStateWithLifecycle()
Column {
Text("Count: ${state.count}")
Button(onClick = { store.dispatch(CounterIntention.Increment) }) {
Text("+")
}
}
}That's it. @AutoState generates CounterStateStore and @AutoFeature generates the Hilt bindings.
| Module | Description | Platform |
|---|---|---|
yamv-core |
Marker interfaces & annotations (State, Outcome, @AutoState, @AutoFeature) |
Pure Kotlin |
yamv |
Core runtime (MviRuntime, MviStore, FeatureRouter) |
Kotlin Multiplatform |
yamv-retainer |
MviRetainedStore — lifecycle-retained ViewModel base |
Android + iOS |
yamv-hilt |
hiltMviStore() Compose helper |
Android |
yamv-koin |
koinMviStore() / mviStore {} DSL |
Kotlin Multiplatform |
yamv-processor-core |
KSP utilities (DI-agnostic) | JVM |
yamv-processor-hilt |
Hilt KSP processor — generates *Store + *FeaturesModule
|
JVM |
Full documentation: ktomek.github.io/yamv
Copyright 2024 Tomasz Kaszkowiak
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0