Implementation of the "One-Time Event" pattern for Android and Kotlin Multiplatform.
Typical usage is ViewModel with Jetpack Compose / Compose Multiplatform.
Outdated as of 2025.
Solutions based on Channels have a limitation: they must operate on Main / Main.immediate scheduler to be reliable.
More information:
ViewModel: One-off event antipatterns
A Channel doesn’t guarantee the delivery and processing of the events. Therefore, events can be lost, leaving the UI in an inconsistent state.
The most trivial solution is to rely on the specifics of how view lifecycle works in Android and on Dispatchers.Main.immediate.
ViewModel: Events as State are an Antipattern
Exposing events from the VM does not mean that the VM is not the source of truth.
It works but requires additional code which can be tedious.
Google's approach (explicitly consume events)
UI needs to notify the ViewModel to trigger another state update when the message has been shown on the screen.
Key features:
- It is guaranteed that a pushed even will not be lost.
- Many events may be queued until someone collects them.
- New event can be pushed from any thread / dispatcher.
- Collection may be happening from any thread / dispatcher.
- Order of events is not guaranteed if pushed from multiple threads / dispatchers.
Limitations:
- Only 1 collector at a time is supported.
How and why it works:
Events are stored in the internal queue. Any access to the queue happens under the Mutex. Any event that was not consumed waits in the queue. Collector automatically and atomically consumes the queue.
It's partially similar to what Google recommends (notify about the consumption), but automatic.
The library is not published. Basically, it's just 1 class, so you can copy it to your project.
Typically in ViewModel:
private val _events = SingleEventStream<Any>(viewModelScope)
val events = _events.asReadOnly()
fun onClick() {
_events.push("Something")
}
Typically in a Composable:
val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) {
lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.events.collect {
// Handle the events here
}
}
}