Getting Started
Prerequisites
- Android Studio Hedgehog or newer
- Kotlin 2.1+ (project itself builds on 2.3.20)
- KSP plugin (for
@AutoState/@AutoFeaturecode generation) — pin a version that matches your Kotlin version
Installation
Step 1: Add KSP plugin
In your app module's build.gradle.kts:
plugins {
// Use a KSP version that matches your Kotlin version (e.g. "2.3.20-2.3.2")
id("com.google.devtools.ksp") version "<KSP_VERSION>"
}
Step 2: Add dependencies
Your First Feature
1. Define State
import com.ktomek.yamv.core.State
import com.ktomek.yamv.annotations.AutoState
@AutoState
data class CounterState(val count: Int = 0) : State
@AutoState triggers KSP to generate CounterStateStore — a @HiltViewModel subclass of MviRetainedStore<CounterState, Any>.
2. Define Intentions
sealed class CounterIntention {
data object Increment : CounterIntention()
data object Decrement : CounterIntention()
data class SetValue(val value: Int) : CounterIntention()
}
3. Define Outcomes
Declare outcomes as standalone classes. They are pure (S) -> S functions, decoupled from features and testable without coroutines:
import com.ktomek.yamv.core.StateOutcome
class IncrementOutcome : StateOutcome<CounterState> {
override fun reduce(prevState: CounterState) =
prevState.copy(count = prevState.count + 1)
}
class DecrementOutcome : StateOutcome<CounterState> {
override fun reduce(prevState: CounterState) =
prevState.copy(count = prevState.count - 1)
}
class SetValueOutcome(private val value: Int) : StateOutcome<CounterState> {
override fun reduce(prevState: CounterState) =
prevState.copy(count = value)
}
4. Write Features
Each feature maps intentions to outcomes:
import com.ktomek.yamv.annotations.AutoFeature
import com.ktomek.yamv.feature.TypedFeature
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@AutoFeature
class IncrementFeature : TypedFeature<CounterState, CounterIntention.Increment> {
override fun invoke(
intentions: Flow<CounterIntention.Increment>
): Flow<Outcome<CounterState>> =
intentions.map { IncrementOutcome() }
}
@AutoFeature
class DecrementFeature : TypedFeature<CounterState, CounterIntention.Decrement> {
override fun invoke(
intentions: Flow<CounterIntention.Decrement>
): Flow<Outcome<CounterState>> =
intentions.map { DecrementOutcome() }
}
5. Collect State in Compose
App-lifetime state across navigation
By default both hiltMviStore() and koinMviStore() resolve through LocalViewModelStoreOwner.current, which Compose Navigation overrides per back-stack entry — every destination gets its own store. For state that should survive navigation (auth, session, preferences):
Resolves the host ComponentActivity automatically and uses it as the MviStoreOwner — same instance shared across the activity's lifetime, no setup required.
For non-default scopes (a specific Fragment, a custom NavBackStackEntry, tests) wrap the subtree with ProvideAppMviStoreOwner(owner) { … } to override LocalAppMviStoreOwner.
Both hiltMviStore and koinMviStore also accept an explicit owner parameter when you need to scope to something other than the default.
Running the Sample App
The :app:hilt module is a working counter demo: