Skip to main content
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)
    }
}
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

1

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
)
2

Define the ViewModel interface

Extend StateViewModel and/or EventViewModel:
interface CartViewModel : StateViewModel<CartScreenState> {
    fun updateCartItemQuantity(cartItem: CartItem)
    fun removeItem(itemId: String)
}
3

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
    }
}
4

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

1

Initialize state early

Always call setDefaultState() in the ViewModel’s init block:
init {
    stateDelegate.setDefaultState(InitialState)
}
2

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))
3

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)
}
4

Single source of truth

Expose only StateFlow/Flow (read-only) to the UI, keep mutable versions private.

Build docs developers (and LLMs) love