PulseViewModel
abstract class PulseViewModel<
UiState : PulseState,
UiAction : PulseAction,
Event : PulseEvent,
Broadcast : PulseBroadcast,
Unicast : PulseUnicast,
>(
initialUiState: UiState,
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,
)Abstract base class that holds the UI state of a content on the desktop, or of a screen on mobile.
The signature above is the desktop's, in mvi-desktop. In mvi-mobile the class takes three type parameters, and unicast, onReceive() and unicast() do not exist; everything else on this page is the same. Use unicast() on the desktop when the ViewModel needs to send messages up to its parent Container.
// mvi-mobile
abstract class PulseViewModel<
UiState : PulseState,
UiAction : PulseAction,
Event : PulseEvent,
>(
initialUiState: UiState,
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,
)Properties
state
val state: StateFlow<UiState>The current UI state as a hot, read-only StateFlow. Collecting it does not change the ViewModel lifecycle.
currentState
val currentState: UiStateSynchronous snapshot of the current UI state. Equivalent to state.value.
event
val event: Flow<Event>A cold Flow of one-time side effects emitted via event(). Collected by PulseContent. Each event goes to a single collector and is consumed; it is not replayed to later collectors.
unicast
val unicast: SharedFlow<Unicast>Desktop only. A hot stream of child-to-parent unicasts emitted via unicast().
coroutineScope
val coroutineScope: CoroutineScopeA CoroutineScope backed by SupervisorJob and the dispatcher passed to the constructor. The dispatcher defaults to Dispatchers.Default and can be swapped for Dispatchers.Main or a test dispatcher.
cancel() cancels this scope and replaces it with a fresh one.
Methods
onSetup()
open fun onSetup()Called once, by PulseContent, the first time it observes the ViewModel. The instance outlives the composition, so leaving and re-entering it does not repeat the call.
Override it to start data-collection coroutines. They run in coroutineScope and stop when it is cancelled.
Observe an instance from one PulseContent at a time. event is a single-consumer channel, so a second observer would take events the first never sees.
onAction(uiAction)
abstract fun onAction(uiAction: UiAction)Called each time the UI dispatches an action. Launch coroutines for async work here.
onReceive(broadcast)
open fun onReceive(broadcast: Broadcast)Desktop only. Called when the parent PulseContainer delivers a broadcast. Default implementation does nothing.
update(block)
fun update(block: UiState.() -> UiState)Applies an immutable state update. The lambda receives the current state as this and must return the next state:
update { copy(count = count + 1) }event(effect)
fun event(effect: Event)Emits a one-time side effect to the UI layer. Collected by the onEvent lambda in PulseContent.
Buffered, so it never suspends and keeps emission order.
Events emitted while no PulseContent is collecting — the destination is covered by another one, say — wait in the buffer and arrive when a collector returns. The buffer holds 64; beyond that, the oldest event is dropped.
unicast(unicast)
fun unicast(unicast: Unicast)Desktop only. Emits a child-to-parent message. The parent PulseContainer collects the ViewModel's unicast flow and receives the message through onReceived().
cancel()
fun cancel()Cancels the work started in onSetup() and replaces the scope with a fresh one, with the state preserved. The next PulseContent to observe the instance runs onSetup() again. Use it when the ViewModel may become active again.
close()
fun close()Cancels the work started in onSetup() for good. Unlike cancel(), it does not replace the scope. onCleared() calls it when the owning ViewModelStore is cleared, so a discarded ViewModel cannot launch anything that outlives it.
Example
sealed class CounterUnicast : PulseUnicast {
data object ResetRequested : CounterUnicast()
}
class CounterViewModel(
private val repository: CounterRepository,
) : PulseViewModel<CounterState, CounterAction, CounterEvent, CounterBroadcast, CounterUnicast>(
initialUiState = CounterState(),
) {
override fun onSetup() {
coroutineScope.launch {
repository.count.collect { count ->
update { copy(count = count) }
}
}
}
override fun onAction(uiAction: CounterAction) {
coroutineScope.launch {
when (uiAction) {
CounterAction.Increment -> repository.increment()
CounterAction.Decrement -> repository.decrement()
}
}
}
override fun onReceive(broadcast: CounterBroadcast) {
when (broadcast) {
CounterBroadcast.Reset -> update { CounterState() }
}
}
}sealed class CounterUnicast : PulseUnicast {
data object ResetRequested : CounterUnicast()
}
class CounterViewModel(
private val repository: CounterRepository,
) : PulseViewModel<CounterState, CounterAction, CounterEvent, CounterBroadcast, CounterUnicast>(
initialUiState = CounterState(),
) {
override fun onAction(uiAction: CounterAction) {
when (uiAction) {
CounterAction.Reset -> {
repository.reset()
unicast(CounterUnicast.ResetRequested)
}
CounterAction.Increment -> repository.increment()
CounterAction.Decrement -> repository.decrement()
}
}
}