Skip to content

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:

@AutoState
data class CounterState(val count: Int = 0) : State

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(),
)