Unicast
Unicast sends a typed message from a PulseViewModel up to its parent PulseContainer.
Use it when handling an action and coordinating the follow-up are split. The child ViewModel owns the immediate user action. The parent Container handles the follow-up, such as broadcasting the result to sibling ViewModels.
Defining a Unicast
Implement PulseUnicast with a sealed class whose cases are data classes or data objects. The Unicast type is shared by the Container and every ViewModel registered in it. This generic pairing means a ViewModel cannot emit a Unicast type that the Container does not understand.
sealed class CounterUnicast : PulseUnicast {
data class CounterUpdated(val count: Int) : CounterUnicast()
}class CounterViewModel : PulseViewModel<
CounterState,
CounterAction,
CounterEvent,
CounterBroadcast,
CounterUnicast,
>(initialUiState = CounterState())class CounterContainer(
viewModels: List<PulseViewModel<*, *, *, CounterBroadcast, CounterUnicast>>,
) : PulseContainer<CounterBroadcast, CounterUnicast>(viewModels = viewModels)Sending a Unicast
Call unicast() from inside a ViewModel. PulseContainer collects each registered ViewModel's unicast flow internally.
override fun onAction(uiAction: CounterAction) {
when (uiAction) {
CounterAction.Increment -> {
repository.increment()
unicast(CounterUnicast.CounterUpdated(repository.count.value))
}
CounterAction.Decrement -> {
repository.decrement()
unicast(CounterUnicast.CounterUpdated(repository.count.value))
}
CounterAction.Reset -> {
repository.reset()
unicast(CounterUnicast.CounterUpdated(repository.count.value))
}
}
}Receiving a Unicast
Override onReceived() in the Container. In the example below, the Container takes the Unicast it received from a ViewModel and sends it back out to all ViewModels as a Broadcast.
override fun onReceived(unicast: CounterUnicast) {
when (unicast) {
is CounterUnicast.CounterUpdated ->
broadcast(CounterBroadcast.CounterUpdated(unicast.count))
}
}Unicast vs Broadcast vs Event
| Unicast | Broadcast | Event | |
|---|---|---|---|
| Direction | ViewModel -> Container | Container -> all ViewModels | ViewModel -> UI |
| Cardinality | Many-to-one | One-to-many | One-to-one |
| Purpose | Parent coordination after a child action | Cross-ViewModel notification | One-time UI side effects |
| Type parameter | PulseUnicast | PulseBroadcast | PulseEvent |
Example: Sharing Counter Updates
Two counter ViewModels keep separate local repositories, yet share updates through their parent Container:
sealed class CounterBroadcast : PulseBroadcast {
data class CounterUpdated(val count: Int) : CounterBroadcast()
}
sealed class CounterUnicast : PulseUnicast {
data class CounterUpdated(val count: Int) : CounterUnicast()
}class CounterContainer(
viewModels: List<PulseViewModel<*, *, *, CounterBroadcast, CounterUnicast>>,
) : PulseContainer<CounterBroadcast, CounterUnicast>(viewModels = viewModels) {
override fun onReceived(unicast: CounterUnicast) {
when (unicast) {
is CounterUnicast.CounterUpdated ->
broadcast(CounterBroadcast.CounterUpdated(unicast.count))
}
}
}class CounterViewModel(
private val repository: CounterRepository,
) : PulseViewModel<CounterState, CounterAction, CounterEvent, CounterBroadcast, CounterUnicast>(
initialUiState = CounterState(),
) {
override fun onAction(uiAction: CounterAction) {
when (uiAction) {
CounterAction.Increment -> {
repository.increment()
unicast(CounterUnicast.CounterUpdated(repository.count.value))
}
CounterAction.Decrement -> {
repository.decrement()
unicast(CounterUnicast.CounterUpdated(repository.count.value))
}
CounterAction.Reset -> {
repository.reset()
unicast(CounterUnicast.CounterUpdated(repository.count.value))
}
}
}
override fun onReceive(broadcast: CounterBroadcast) {
when (broadcast) {
is CounterBroadcast.CounterUpdated -> repository.set(broadcast.count)
}
}
}