The ViewModel library provides abstractions for managing UI state and events in a framework-agnostic way, using Kotlin Flow for reactive programming.
Core Interfaces
StateViewModel
Interface for ViewModels that expose a reactive state.
interface StateViewModel < State > {
val state: StateFlow < State >
}
Purpose:
Exposes UI state as a StateFlow
Provides read-only access to state from the UI layer
Ensures single source of truth for state
EventViewModel
Interface for ViewModels that emit one-time events.
interface EventViewModel < ViewEvent > {
val viewEvent: Flow < ViewEvent >
}
Purpose:
Handles one-time UI events (navigation, showing dialogs, etc.)
Uses Flow instead of StateFlow to avoid event replay
Separates events from state for clearer architecture
Delegate Implementations
StateDelegate
Reusable delegate for managing state in ViewModels.
class StateDelegate < State > : StateViewModel < State > {
private lateinit var _state: MutableStateFlow < State >
override val state: StateFlow < State >
get () = _state. asStateFlow ()
fun setDefaultState (state: State ) {
_state = MutableStateFlow (state)
}
fun updateState (block: ( State ) -> State ) {
_state. update {
block (it)
}
}
}
Key Methods:
setDefaultState(): Initialize the state (typically in ViewModel init block)
updateState(): Atomically update state using the current value
EventDelegate
Reusable delegate for emitting one-time events from ViewModels.
class EventDelegate < ViewEvent > : EventViewModel < ViewEvent > {
private val _viewEvent = MutableSharedFlow < ViewEvent >()
override val viewEvent: Flow < ViewEvent > = _viewEvent. asSharedFlow ()
fun sendEvent (scope: CoroutineScope , newEvent: ViewEvent ) {
scope. launch {
_viewEvent. emit (newEvent)
}
}
}
Key Methods:
sendEvent(): Emit a one-time event to the UI layer
Uses MutableSharedFlow instead of MutableStateFlow to ensure events are not replayed when new collectors subscribe.
Usage Examples
Basic ViewModel with State
Example from CartViewModel showing state-only ViewModel:
internal interface CartViewModel : StateViewModel < CartScreenState > {
fun updateCartItemQuantity (cartItem: CartItem )
}
internal data class CartScreenState ( val cart: Cart )
ViewModel Implementation with StateDelegate
Example from RealCartViewModel showing delegation pattern:
internal class RealCartViewModel (
observeUserCart: ObserveUserCart ,
private val updateCartItem: UpdateCartItem ,
private val stateDelegate: StateDelegate < CartScreenState >
) : CartViewModel , StateViewModel < CartScreenState > by stateDelegate, ViewModel () {
init {
stateDelegate. setDefaultState ( CartScreenState ( Cart ( emptyList ())))
observeUserCart (). onEach { cart ->
stateDelegate. updateState { CartScreenState (cart) }
}. launchIn (viewModelScope)
}
override fun updateCartItemQuantity (cartItem: CartItem ) {
updateCartItem (cartItem)
}
}
Delegation Pattern Explained
The ViewModel uses Kotlin’s delegation (by stateDelegate) to implement the StateViewModel interface:
StateDelegate is injected as a dependency
The by keyword delegates the state property to the delegate
Internal methods like updateState() are called directly on the delegate
This separates state management logic from business logic
Step-by-Step ViewModel Creation
Define the state model
Create a data class representing your screen state: data class CartScreenState (
val cart: Cart ,
val isLoading: Boolean = false ,
val error: String ? = null
)
Define the ViewModel interface
Extend StateViewModel and/or EventViewModel: interface CartViewModel : StateViewModel < CartScreenState > {
fun updateCartItemQuantity (cartItem: CartItem )
fun removeItem (itemId: String )
}
Implement the ViewModel
Use delegation for state management: class RealCartViewModel (
private val useCases: CartUseCases ,
private val stateDelegate: StateDelegate < CartScreenState >
) : CartViewModel ,
StateViewModel < CartScreenState > by stateDelegate,
ViewModel () {
init {
stateDelegate. setDefaultState ( CartScreenState ( Cart ( emptyList ())))
// Initialize state from use cases
}
override fun updateCartItemQuantity (cartItem: CartItem ) {
// Implement business logic
}
}
Observe state in UI
Collect the state in your composable or activity: @Composable
fun CartScreen (viewModel: CartViewModel ) {
val state by viewModel.state. collectAsStateWithLifecycle ()
// Render UI based on state
CartContent (
cart = state.cart,
onItemUpdate = viewModel:: updateCartItemQuantity
)
}
ViewModel with Events
Example showing both state and events:
sealed class ProductEvent {
data class ShowError ( val message: String ) : ProductEvent ()
data class NavigateToDetail ( val productId: String ) : ProductEvent ()
}
data class ProductListState (
val products: List < Product > = emptyList (),
val isLoading: Boolean = false
)
class ProductViewModel (
private val getProducts: GetProducts ,
private val stateDelegate: StateDelegate < ProductListState >,
private val eventDelegate: EventDelegate < ProductEvent >
) : StateViewModel < ProductListState > by stateDelegate ,
EventViewModel < ProductEvent > by eventDelegate,
ViewModel () {
init {
stateDelegate. setDefaultState ( ProductListState ())
loadProducts ()
}
private fun loadProducts () {
viewModelScope. launch {
stateDelegate. updateState { it. copy (isLoading = true ) }
when ( val result = getProducts ()) {
is Answer.Success -> {
stateDelegate. updateState {
it. copy (
products = result. data ,
isLoading = false
)
}
}
is Answer.Error -> {
stateDelegate. updateState { it. copy (isLoading = false ) }
eventDelegate. sendEvent (
viewModelScope,
ProductEvent. ShowError ( "Failed to load products" )
)
}
}
}
}
fun onProductClick (productId: String ) {
eventDelegate. sendEvent (
viewModelScope,
ProductEvent. NavigateToDetail (productId)
)
}
}
Observing Events in UI
@Composable
fun ProductListScreen (viewModel: ProductViewModel ) {
val state by viewModel.state. collectAsStateWithLifecycle ()
val navController = LocalNavController.current
LaunchedEffect (Unit) {
viewModel.viewEvent. collect { event ->
when (event) {
is ProductEvent.ShowError -> {
// Show snackbar or dialog
}
is ProductEvent.NavigateToDetail -> {
navController. navigate ( "product/ ${ event.productId } " )
}
}
}
}
ProductListContent (
state = state,
onProductClick = viewModel:: onProductClick
)
}
Always collect events in a LaunchedEffect to ensure proper lifecycle handling and avoid memory leaks.
Testing ViewModels
Testing State Updates
class CartViewModelTest {
private val observeUserCart = mock < ObserveUserCart >()
private val updateCartItem = mock < UpdateCartItem >()
private val stateDelegate = StateDelegate < CartScreenState >()
private lateinit var viewModel: RealCartViewModel
@Test
fun `updates state when cart changes` () = runTest {
val cartFlow = MutableStateFlow ( Cart ( emptyList ()))
whenever ( observeUserCart ()). thenReturn (cartFlow)
viewModel = RealCartViewModel (
observeUserCart,
updateCartItem,
stateDelegate
)
val newCart = Cart ( listOf (cartItem))
cartFlow. emit (newCart)
assertEquals (newCart, viewModel.state. value .cart)
}
}
Inject delegates as dependencies to enable easier testing and verification of state changes.
Architecture Benefits
Framework Agnostic Not tied to Android’s ViewModel - can be used in any Kotlin context including KMP.
Separation of Concerns Delegates separate state management from business logic, making ViewModels cleaner.
Testable Delegates can be easily mocked or replaced in tests for isolated unit testing.
Reusable Delegates can be reused across multiple ViewModels without duplicating code.
When to Use State vs Events
Use State for: Persistent UI data that can be re-rendered (loading states, data lists, form values)
Use Events for: One-time actions that shouldn’t replay (navigation, showing toasts, triggering animations)
Best Practices
Initialize state early
Always call setDefaultState() in the ViewModel’s init block: init {
stateDelegate. setDefaultState (InitialState)
}
Use updateState for modifications
Prefer updateState() over directly setting state for atomic updates: // Good
stateDelegate. updateState { it. copy (isLoading = true ) }
// Avoid - not atomic
stateDelegate. setDefaultState (state. value . copy (isLoading = true ))
Immutable state
Use data classes and copy for state updates to ensure immutability: data class State ( val items: List < Item >)
stateDelegate. updateState {
it. copy (items = it.items + newItem)
}
Single source of truth
Expose only StateFlow/Flow (read-only) to the UI, keep mutable versions private.