Blog Infos
Author
Published
Topics
, , , ,
Published

Master Kotlin’s select expression for precise Snackbar timing beyond Short and Long durations

 

Introduction

Did you ever want your Snackbar to display for exactly 6.5 seconds instead of the standard “Short” or “Long” durations? Material3’s Snackbar component restricts you to three fixed timing options, but there’s an elegant workaround using Kotlin’s powerful select coroutine function.

The Problem with Fixed Durations

Material3 limits Snackbar timing to three predefined values:

  • Short — around 4 seconds
  • Long — around 10 seconds
  • Indefinite — persists until manual dismissal

This becomes problematic when your app requires specific timing, such as “File will be deleted in 6.5 seconds” — it’s not possible to achieve this with the standard API.

The Solution: Racing User Actions Against Timers

Custom timing is essentially a race between two events:

  1. User response — clicks action, swipes away, or taps outside
  2. Timer expires — your custom duration completes

Kotlin’s select coroutine construct manages exactly this competitive scenario.

Implementation: The Duration Wrapper

First, we need a wrapper that handles both standard and custom durations:

sealed class SnackbarDurationWrapper {
data class Standard(val duration: SnackbarDuration) : SnackbarDurationWrapper()
data class Custom(val millis: Long) : SnackbarDurationWrapper()
companion object {
fun fromMillis(milliseconds: Long): SnackbarDurationWrapper {
require(milliseconds > 0) { "Duration must be positive" }
return Custom(milliseconds)
}
fun fromSeconds(seconds: Double): SnackbarDurationWrapper {
return Custom((seconds * 1000).toLong())
}
}
fun getMilliseconds(): Long {
return when (this) {
is Standard -> when (duration) {
SnackbarDuration.Short -> 4000L
SnackbarDuration.Long -> 10000L
SnackbarDuration.Indefinite -> Long.MAX_VALUE
}
is Custom -> millis
}
}
}
The Magic: Koltin’s select expression-based racing

Here’s how we use select to handle the race between user interaction and timer expiration:

LaunchedEffect(currentMessage.value) {
currentMessage.value?.let { message ->
val result: SnackbarResult
if (message.duration is SnackbarDurationWrapper.Custom) {
// Create two competing operations
val snackbarDeferred = async {
snackbarHostState.showSnackbar(
message = message.text,
actionLabel = message.actionLabel,
duration = SnackbarDuration.Indefinite // Never auto-dismiss
)
}
val timeoutDeferred = async {
delay(message.duration.getMilliseconds())
SnackbarResult.Dismissed // Timer won
}
// Race them - winner takes all
result = select {
snackbarDeferred.onAwait { userResult ->
timeoutDeferred.cancel() // Cancel timer
userResult
}
timeoutDeferred.onAwait { timerResult ->
snackbarHostState.currentSnackbarData?.dismiss()
snackbarDeferred.cancel() // Cancel snackbar
timerResult
}
}
} else {
// Use standard Material3 behavior
result = snackbarHostState.showSnackbar(
message = message.text,
actionLabel = message.actionLabel,
duration = message.duration.getStandardDuration()
)
}
// Handle the result
when (result) {
SnackbarResult.ActionPerformed -> message.onAction?.invoke()
SnackbarResult.Dismissed -> { /* Handle dismissal */ }
}
}
}
Why This Works

The select-based approach provides several key advantages:

Perfect Race Management

select automatically handles the competition between user interaction and timer expiration, providing deterministic behavior regardless of which event completes first.

Zero Memory Leaks

When one operation completes, select automatically cancels the other, preventing abandoned coroutines from consuming resources.

Millisecond Precision

Unlike Material3’s approximate timing, this delivers exact duration control down to the millisecond.

Seamless Integration

Works within Material3’s existing architecture — no changes to Compose’s snackbar system required.

Practical Usage Examples
// Undo action with precise 5-second window
showCustomSnackbar(
message = "Item deleted",
actionLabel = "Undo",
onAction = { restoreItem() },
durationMillis = 5000
)
// Quick success confirmation
showCustomSnackbar(
message = "File saved successfully",
durationMillis = 2000
)
// Critical error with extended display time
showCustomSnackbar(
message = "Payment failed - contact support",
durationMillis = 12000
)

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Kotlin Coroutine Mechanisms: A Surprisingly Deep Rabbithole

Sometimes you think you know coroutines, and then after a while, you’re like, “Wait, do I really know coroutines?”
Watch Video

Kotlin Coroutine Mechanisms: A Surprisingly Deep Rabbithole

Amanda Hinchman-Dominguez
Senior Android Developer
SpotOn

Kotlin Coroutine Mechanisms: A Surprisingly Deep Rabbithole

Amanda Hinchman-Do ...
Senior Android Devel ...
SpotOn

Kotlin Coroutine Mechanisms: A Surprisingly Deep Rabbithole

Amanda Hinchman- ...
Senior Android Developer
SpotOn

Jobs

Beyond Snackbars

The select pattern applies to any UI scenario involving race conditions between user actions and timers — tooltips, dialogs, loading states, or any component where precise timing matters.

Conclusion

By leveraging Kotlin’s select function, you can break free from Material3’s timing constraints while maintaining clean, readable code. This approach provides the precision your app needs while preserving the Material3 user experience.

This article was previously published on proandroiddev.com.

Menu