SnackbarChannel
⚠️ Limitation (and Upcoming Fix)
Thanks to the community feedback, I learned that this solution can lose the Snackbar if a configuration change happens right after the event is delivered to the collector but before the Snackbar is dismissed. As a result, the Snackbar won't be shown again even if the user hasn't grasped the message yet. Therefore, I'll switch back to StateFlow<List<Event>> but will abstract out the details to make it easy for the developers. The update should be available before September 2025.
A lightweight, lifecycle-safe snackbar event dispatcher for Compose Multiplatform that addresses common pitfalls of using SharedFlow and StateFlow.
⚠️ Using Jetpack Compose for Android only?
This library relies on StringResource and getString from org.jetbrains.compose.resources, which are not supported in pure Android projects. Please refer to the Android-specific version instead: AndroidSnackbarChannel.
Why use SnackbarChannel?
SnackbarChannel addresses the common pitfalls of using StateFlow, SharedFlow, or even StateFlow<List<Event>>.
StateFlow re-emits on config changes, leading to duplicate snackbars.
SharedFlow(replay = 0) may drop events when no collector is active.
SharedFlow(replay = 1) can treat each lifecycle change as a new subscription, re-emitting events.
- List-based workarounds (e.g.,
StateFlow<List<String>>) require manual state updates to remove consumed items, adding extra complexity and visual noise to the .
SnackbarChannel avoids these issues:
- Emits events reliably even across lifecycle changes.
- No risk of duplicates or dropped events.
- No manual list mutation or bookkeeping required.
It’s a focused solution that keeps your snackbar logic clean, lifecycle-aware, and easy to use.
Features
- One-liner API for triggering snackbars from your
ViewModel
- No more missed or duplicated snackbars
- Lifecycle-aware: events are only collected when the UI is active
- Works seamlessly with
SnackbarHostState.showSnackbar(...)
- No brittle base classes - favors composition over inheritance using Kotlin delegation
- Supports all original showSnackbar parameters (action labels, duration, callbacks, etc.)
- Compose Multiplatform support
Installation
commonMain.dependencies {
implementation("io.github.aungthiha:snackbar-channel:1.0.7")
}
Setup
1. Add SnackbarChannel to your ViewModel
By default, SnackbarChannel uses Channel.UNLIMITED and BufferOverflow.SUSPEND to ensure no snackbar events are dropped.
import io.github.aungthiha.snackbar.SnackbarChannel
import io.github.aungthiha.snackbar.SnackbarChannelOwner
class MyViewModel(
private val snackbarChannel: SnackbarChannel = SnackbarChannel()
) : ViewModel(), SnackbarChannelOwner by snackbarChannel {
fun showSimpleSnackbar() {
showSnackBar(message = Res.string.hello_world)
}
}
While the defaults are recommended for most use cases, both the channel capacity and onBufferOverflow strategy are configurable:
SnackbarChannel(
capacity = Channel.RENDEZVOUS,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
2. Observe snackbars in your Composable
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import io.github.aungthiha.snackbar.collectWithLifecycle
import io.github.aungthiha.snackbar.showSnackbar
@Composable
fun MyScreen(viewModel: MyViewModel = viewModel()) {
val snackbarHostState = remember { SnackbarHostState() }
viewModel.snackbarFlow.observeWithLifecycle {
snackbarHostState.showSnackbar(it)
}
Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
Button(onClick = { viewModel.showSimpleSnackbar() }) {
Text()
}
}
}
API Overview
Use showSnackBar(...) from your ViewModel. You can pass string resources, string literals, or even mix both using SnackbarString.
showSnackBar(
message = Res.string.hello_world,
actionLabel = "ok",
withDismissAction = true,
duration = SnackbarDuration.Indefinite,
onActionPerform = { },
onDismiss = { }
)
showSnackBar(
message = Res.string.hello_world,
actionLabel = Res.string.ok
)
showSnackBar(
message = ,
actionLabel =
)
showSnackBar(
message = ,
actionLabel = Res.string.ok
)
showSnackBar(
message = Res.string.hello_world,
actionLabel =
)
All parameters are optional except the message.
For more example usages, see AppViewModel.kt
Unit Testing
You can test snackbar emissions using runTest and collecting from snackbarFlow.
class MyViewModelTest {
private val viewModel = MyViewModel()
@Test
fun snackbar_is_emitted() = runTest {
viewModel.showSimpleSnackbar()
val snackbarModel = viewModel.snackbarFlow.first()
assertEquals(
SnackbarString(Res.string.hello_world),
snackbarModel.message
)
}
}
Compose Multiplatform Ready
Tested with:
(Other targets are available but not tested yet)
Contributing
PRs and feedback welcome!
License
MIT