viewmodeldelegates
0.3.0indexedDelegate-based ViewModel composition enabling smaller, testable MVVM modules; unidirectional state flow, pure delegates, explicit state/effect handling, fast builds and Compose-friendly UI binding.
Delegate-based ViewModel composition enabling smaller, testable MVVM modules; unidirectional state flow, pure delegates, explicit state/effect handling, fast builds and Compose-friendly UI binding.
Architecture for Android applications in Kotlin using the MVVM pattern.**
Groovy DSL:
dependencies {
implementation platform('com.yugyd.viewmodeldelegates:viewmodeldelegates-bom:{latest_version}')
implementation 'com.yugyd.viewmodeldelegates:viewmodeldelegates'
implementation 'com.yugyd.viewmodeldelegates:viewmodeldelegates-ui'
}
Kotlin DSL:
dependencies {
implementation(platform("com.yugyd.viewmodeldelegates:viewmodeldelegates-bom:{latest_version}"))
implementation("com.yugyd.viewmodeldelegates:viewmodeldelegates")
implementation("com.yugyd.viewmodeldelegates:viewmodeldelegates-ui")
}
Why: the library is split conceptually into:
StateCreate a single immutable state for the screen.
data class State(
val arguments: Arguments = Arguments(),
isLoading: = ,
isWarning: = ,
: String = ,
navigationState: NavigationState? = ,
) {
( userName: String = )
{
NavigateToFavourites : NavigationState
}
}
Why:
copy() makes updates explicit and safe.navigationState and showErrorMessage model one-time effects (more on that later).EventDefine all inputs as a sealed interface/class:
sealed interface Event {
object LoadData : Event
object OnActionClicked : Event
object OnSnackbarDismissed : Event
object OnNavigationHandled : Event
}
Why: UI communicates only via events; no direct mutation, no “call random method” style API.
interface SampleViewModel : JvmViewModel<Event, State> {
// Add State/Events here for encapsulation
}
In the sample, the contract embeds Event and State inside the interface; that’s a good practice
for feature encapsulation.
class OnNavigationHandledViewModelDelegate : SampleViewModelDelegate {
override fun accept(
event: Event,
viewModel: ViewModelDelegates<, State>,
scope: ,
getState: () ->
): {
(event != Event.OnNavigationHandled)
viewModel.updateState { it.copy(navigationState = ) }
}
}
Why this structure is important:
scope.updateState { }.true if handled, otherwise).The sample uses a builder function (can be replaced by DI framework):
fun buildSampleBinder(): SampleBinder {
// ...
val viewModel = object : SampleViewModel,
JvmViewModel<Event, State> by DefaultViewModelFactory().create(
initialState = State(arguments = arguments),
viewModelDelegates = setOf(
LoadDataViewModelDelegate(repository),
OnActionClickedViewModelDelegate(),
OnNavigationHandledViewModelDelegate(),
OnSnackbarDismissedViewModelDelegate(),
),
initEvents = setOf(Event.LoadData),
logger = buildLogger(),
name = "SampleViewModel",
) {}
return SampleBinder(
viewModel = viewModel,
mapper = SampleMapper(),
)
}
Why:
initEvents = setOf(Event.LoadData) triggers initial loading automatically.by factory.create(...) avoids boilerplate while still exposing a typed
interface.Sample SampleBinder.Model:
data class Model(
val isLoading: Boolean = false,
val isWarning: Boolean = false,
val data: String = "",
val navigationState: NavigationUiState? = null,
) {
{
NavigateToFavourites : NavigationUiState
}
}
Why:
NavigationState annotated with @Immutable to eliminate mapping and make the
code simpler.class SampleMapper : StateToModelMapper<State, Model> {
override fun map(state: State): Model {
return Model(
isLoading = state.isLoading,
isWarning = state.isWarning,
data = state.data,
navigationState = when (state.navigationState) {
State.NavigationState.NavigateToFavourites -> Model.NavigationUiState.NavigateToFavourites
->
},
)
}
}
Why: mapping isolates UI from domain changes and keeps Compose code simple.
class SampleBinder(
private val viewModel: SampleViewModel,
mapper: SampleMapper,
) : ModelViewModelBinder<Event, State, Model>(
viewModel = viewModel,
initialModel = Model(),
stateToModelMapper = mapper,
) {
fun onActionClicked() = viewModel.accept(Event.OnActionClicked)
fun onSnackbarDismissed() = viewModel.accept(Event.OnSnackbarDismissed)
fun = viewModel.accept(Event.OnNavigationHandled)
}
Why:
model as a stream for Compose.@Composable
fun SampleScreen(binder: SampleBinder) {
val state by binder.model.collectAsStateWithLifecycle()
// ...
}
class SimpleHomeBinder(
private val viewModel: HomeViewModel,
) : StateViewModelBinder<Event, State>(viewModel) {
fun onEvent(event: Event) = viewModel.accept(event)
}
State immutable and updated only via copy.Event is handled by exactly one delegate.
setOf(...) (unordered).scope for async operations (it is lifecycle-bound).getState() inside coroutines when you need fresh state values.StateToModelMapper to keep Compose simple and stable.In a typical MVVM project, ViewModels tend to grow into “God objects”:
when(event) (or dozens of public methods),View Model Delegates standardizes ViewModel logic as a composition of small event handlers ( “delegates”), while keeping:
It enforces a predictable “unidirectional” flow:
UI → Event → Delegate → State update → UI re-render
and improves maintainability by making your ViewModel:
The sample project demonstrates the usage of the library in a simple screen with loading, warning, data display, snackbar, and navigation.
Copyright 2025 Roman Likhachev
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance the License.
You may obtain a copy of the License at
http:
Unless applicable law agreed to writing, software
distributed under the License distributed an BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express implied.
See the License the specific language governing permissions
limitations under the License.
class LoadDataViewModelDelegate(
private val repository: SampleRepository,
) : SampleViewModelDelegate {
override fun accept(
event: Event,
viewModel: ViewModelDelegates<Event, State>,
scope: CoroutineScope,
getState: () -> State
): Boolean {
if (event != Event.LoadData) return false
// 1) Update state
viewModel.updateState {
it.copy(
isLoading = true,
isWarning = false,
message = "",
showErrorMessage = false,
)
}
// 2) Run async work in the provided scope
scope.launch {
// Add your logic
}
return true
}
}
falseSampleViewModelautoInit = false and trigger init events manually if needed; this is also useful for
mocks in tests.DefaultViewModelFactory can be wrapped in DI framework factories.updateState { copy(...) }CoroutineScope),State → UI Model.Surfaced from shared tags and platforms — no rankings paid for.