Skip to content

Instantly share code, notes, and snippets.

@krizzu
Created July 28, 2024 10:10
Show Gist options
  • Save krizzu/60b7ea7e7865e6495cbd9359f20c4b91 to your computer and use it in GitHub Desktop.
Save krizzu/60b7ea7e7865e6495cbd9359f20c4b91 to your computer and use it in GitHub Desktop.
Hassle-free Snackbar in Jetpack Compose - article at https://www.kborowy.com/blog/easy-compose-snackbar/
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.staticCompositionLocalOf
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlin.coroutines.EmptyCoroutineContext
private val LocalSnackbarController = staticCompositionLocalOf {
SnackbarController(
host = SnackbarHostState(),
scope = CoroutineScope(EmptyCoroutineContext)
)
}
private val channel = Channel<SnackbarChannelMessage>(capacity = Int.MAX_VALUE)
@Composable
fun SnackbarControllerProvider(content: @Composable (snackbarHost: SnackbarHostState) -> Unit) {
val snackHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val snackController = remember(scope) { SnackbarController(snackHostState, scope) }
DisposableEffect(snackController, scope) {
val job = scope.launch {
for (payload in channel) {
snackController.showMessage(
message = payload.message,
duration = payload.duration,
action = payload.action
)
}
}
onDispose {
job.cancel()
}
}
CompositionLocalProvider(LocalSnackbarController provides snackController) {
content(
snackHostState
)
}
}
@Immutable
class SnackbarController(
private val host: SnackbarHostState,
private val scope: CoroutineScope,
) {
companion object {
val current
@Composable
@ReadOnlyComposable
get() = LocalSnackbarController.current
fun showMessage(
message: String,
action: SnackbarAction? = null,
duration: SnackbarDuration = SnackbarDuration.Short,
) {
channel.trySend(
SnackbarChannelMessage(
message = message,
duration = duration,
action = action
)
)
}
}
fun showMessage(
message: String,
action: SnackbarAction? = null,
duration: SnackbarDuration = SnackbarDuration.Short,
) {
scope.launch {
/**
* note: uncomment this line if you want snackbar to be displayed immediately,
* rather than being enqueued and waiting [duration] * current_queue_size
*/
// host.currentSnackbarData?.dismiss()
val result =
host.showSnackbar(
message = message,
actionLabel = action?.title,
duration = duration
)
if (result == SnackbarResult.ActionPerformed) {
action?.onActionPress?.invoke()
}
}
}
}
data class SnackbarChannelMessage(
val message: String,
val action: SnackbarAction?,
val duration: SnackbarDuration = SnackbarDuration.Short,
)
data class SnackbarAction(val title: String, val onActionPress: () -> Unit)
@Composable
fun App() {
SnackbarControllerProvider { host ->
val snackbar = SnackbarController.current
Scaffold(snackbarHost = { SnackbarHost(hostState = host) }) {
Button(onClick = { snackbar.showMessage("hello!") }) {
Text("Click me!")
}
}
}
}
class MainActivity : ComponentActivity() {
override fun onStart() {
super.onStart()
SnackbarController.showMessage("Welcome back!")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment