PulseViewModel
abstract class PulseViewModel<
UiState : PulseState,
UiAction : PulseAction,
Event : PulseEvent,
Broadcast : PulseBroadcast,
Unicast : PulseUnicast,
>(
initialUiState: UiState,
coroutineDispatcher: CoroutineDispatcher = Dispatchers.Default,
)Abstract base class for managing the UI state of a single content.
Use unicast() when the ViewModel needs to send messages up to its parent Container.
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>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)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)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()
}
}
}