Code Generation
YAMV uses KSP (Kotlin Symbol Processing) to generate boilerplate at compile time. You write the annotations; YAMV generates the retained store and DI wiring.
@AutoState
Annotate a State subclass to generate a *Store retained store:
Generated CounterStateStore.kt:
@HiltViewModel
class CounterStateStore @Inject constructor(
features: Set<@JvmSuppressWildcards Feature<CounterState>>,
defaultConfig: Optional<CoroutineDispatcherConfig>,
@MviDispatcherConfig(CounterState::class)
optionalDispatcherConfig: Optional<CoroutineDispatcherConfig>,
) : MviRetainedStore<CounterState, Any>() {
override val dispatcherConfig: CoroutineDispatcherConfig =
optionalDispatcherConfig.orElseGet { defaultConfig.orElseGet { DefaultCoroutineDispatcherConfig() } }
override val store: MviStore<CounterState, Any> = MviRuntime(
features = features,
defaultState = CounterState(),
dispatcherConfig = dispatcherConfig,
)
}
The defaultState is inferred from the primary constructor's default values.
@AutoFeature
Annotate a Feature implementation to include it in the Hilt multibinding set:
@AutoFeature
class IncrementFeature : TypedFeature<CounterState, CounterIntention.Increment> { ... }
Generated inside CounterStateFeaturesModule.kt:
@Module
@InstallIn(ViewModelComponent::class)
interface CounterStateFeaturesModule {
// Direct Feature<S> subtypes (FlowFeature, FlowUnitFeature) are bound abstractly:
@Binds @IntoSet @ViewModelScoped
fun bindsAutoIncreaseFeature(it: AutoIncreaseFeature): Feature<CounterState>
@BindsOptionalOf @MviDispatcherConfig(CounterState::class)
fun bindOptionalDispatcherConfig(): CoroutineDispatcherConfig
companion object {
// Empty-set seed so Dagger doesn't fail when no features are bound:
@Provides @ElementsIntoSet @ViewModelScoped
fun provideDefaults(): Set<Feature<CounterState>> = emptySet()
// Typed feature classes (TypedFeature, FunctionTypedFeature, ActionTypedFeature,
// TypedUnitFeature) need .wrap() — they go in the companion object:
@Provides @IntoSet @ViewModelScoped
fun providesIncrementFeature(it: IncrementFeature): Feature<CounterState> = it.wrap()
}
}
@Binds (abstract) methods go in the interface body; @Provides + .wrap() methods go in the companion object (Dagger constraint).
Using @AutoFeature on Properties
If your feature is a lambda or builder result, annotate the property:
object CounterFeatures {
@AutoFeature
val incrementFeature = functionTypedFeature<CounterState, CounterIntention.Increment> { _ ->
IncrementOutcome()
}
}
@AutoDispatcherConfig
Annotate a CoroutineDispatcherConfig implementation to auto-generate a Dagger module — no manual @Module + @Provides needed:
// Global default — applies to all states without a specific override
@AutoDispatcherConfig
class IoDispatcherConfig : CoroutineDispatcherConfig {
override fun provideIntentionDispatcher(intention: Any?) = Dispatchers.Main
override fun provideReducerDispatcher() = Dispatchers.Main
override fun provideFeatureDispatcher(feature: Any) = Dispatchers.IO
}
Generated IoDispatcherConfigModule.kt:
@Module
@InstallIn(ViewModelComponent::class)
object IoDispatcherConfigModule {
@Provides
fun provideDefault(): CoroutineDispatcherConfig = IoDispatcherConfig()
}
For per-state or multi-state configs, specify the state classes:
@AutoDispatcherConfig(CounterState::class)
class CounterConfig : CoroutineDispatcherConfig { ... }
@AutoDispatcherConfig(TimerState::class, AnimationState::class)
class SharedConfig : CoroutineDispatcherConfig { ... }
Generated (per-state):
@Module
@InstallIn(ViewModelComponent::class)
object CounterConfigModule {
@Provides @MviDispatcherConfig(CounterState::class)
fun provideForCounterState(): CoroutineDispatcherConfig = CounterConfig()
}
Precedence: per-state > global default > DefaultCoroutineDispatcherConfig()
Processor Modules
| Module | Annotation | Output |
|---|---|---|
yamv-processor-hilt |
@AutoState |
{Name}Store (@HiltViewModel) |
yamv-processor-hilt |
@AutoFeature |
{Name}FeaturesModule (Hilt @Module) |
yamv-processor-hilt |
@AutoDispatcherConfig |
{Name}Module (Dagger @Module with @Provides) |
Without Code Generation
If you prefer manual wiring (or use Koin without KSP), extend MviRetainedStore directly and call .wrap() on typed features:
class CounterViewModel(features: Set<Feature<CounterState>>)
: MviRetainedStore<CounterState, Any>() {
override val store = MviRuntime(
features = features,
defaultState = CounterState(),
)
}
// Manual wiring — .wrap() required for TypedFeature / FunctionTypedFeature
val store = MviRuntime(
features = setOf(
IncrementFeature().wrap(),
DecrementFeature().wrap(),
),
defaultState = CounterState(),
)