How to Create a Countdown Snackbar in Android with Jetpack Compose

Kappdev
5 min readJun 23, 2024

--

Welcome 👋

In this article, we’ll create a Countdown Snackbar in Jetpack Compose. This feature is perfect for giving users a moment to rethink crucial actions, like deleting an account.

Stay tuned, and let’s dive in! 🚀

Creating the Snackbar Countdown

To begin, we define the SnackbarCountdown composable function, which visually represents the countdown timer.

@Composable
private fun SnackbarCountdown(
timerProgress: Float,
secondsRemaining: Int,
color: Color
) {
Box(
modifier = Modifier.size(24.dp),
contentAlignment = Alignment.Center
) {
Canvas(Modifier.matchParentSize()) {
// Define the stroke
val strokeStyle = Stroke(
width = 3.dp.toPx(),
cap = StrokeCap.Round
)
// Draw the track
drawCircle(
color = color.copy(alpha = 0.12f),
style = strokeStyle
)
// Draw the progress
drawArc(
color = color,
startAngle = -90f,
sweepAngle = (-360f * timerProgress),
useCenter = false,
style = strokeStyle
)
}
// Display the remaining seconds
Text(
text = secondsRemaining.toString(),
style = LocalTextStyle.current.copy(
fontSize = 14.sp,
color = color
)
)
}
}

Setting Up the CountdownSnackbar Function

Now that we have the SnackbarCountdown composable, we can define the CountdownSnackbar composable, which takes several parameters to customize its appearance and behavior.

@Composable
fun CountdownSnackbar(
snackbarData: SnackbarData,
modifier: Modifier = Modifier,
durationInSeconds: Int = 5,
actionOnNewLine: Boolean = false,
shape: Shape = SnackbarDefaults.shape,
containerColor: Color = SnackbarDefaults.color,
contentColor: Color = SnackbarDefaults.contentColor,
actionColor: Color = SnackbarDefaults.actionColor,
actionContentColor: Color = SnackbarDefaults.actionContentColor,
dismissActionContentColor: Color = SnackbarDefaults.dismissActionContentColor,
) {
// Implementation here...
}

snackbarData Data for the snackbar.

modifier ➜ Modifier to be applied to the snackbar.

durationInSeconds Duration of the countdown timer.

actionOnNewLine Whether to display the action on a separate line.

shape The shape of the snackbar’s container.

containerColor, contentColor, actionColor, actionContentColor, dismissActionContentColor Various color styling parameters.

CountdownSnackbar Implementation

Managing Snackbar Duration and State

Next, we calculate the total duration in milliseconds and manage the remaining time using a state variable. We also use a LaunchedEffect to handle the countdown and dismiss the snackbar when time runs out.

val totalDuration = remember(durationInSeconds) { durationInSeconds * 1000 }
var millisRemaining by remember { mutableIntStateOf(totalDuration) }

LaunchedEffect(snackbarData) {
while (millisRemaining > 0) {
delay(40)
millisRemaining -= 40
}
snackbarData.dismiss()
}

Using a 40-millisecond interval results in a 25 FPS progress update, which is quite smooth for the human eye. Feel free to adjust it to suit your specific needs.

Handling Action & Dismiss Buttons

To create the action and dismiss buttons, we utilize the information provided by snackbarData.

// Define the action button if an action label is provided
val actionLabel = snackbarData.visuals.actionLabel
val actionComposable: (@Composable () -> Unit)? = if (actionLabel != null) {
@Composable {
TextButton(
colors = ButtonDefaults.textButtonColors(contentColor = actionColor),
onClick = { snackbarData.performAction() },
content = { Text(actionLabel) }
)
}
} else {
null
}

// Define the dismiss button if the snackbar includes a dismiss action
val dismissActionComposable: (@Composable () -> Unit)? = if (snackbarData.visuals.withDismissAction) {
@Composable {
IconButton(
onClick = { snackbarData.dismiss() },
content = {
Icon(Icons.Rounded.Close, null)
}
)
}
} else {
null
}

Displaying the Snackbar

Finally, let’s put everything together and display the snackbar.

Snackbar(
modifier = modifier.padding(12.dp), // Apply padding around the snackbar
action = actionComposable,
actionOnNewLine = actionOnNewLine,
dismissAction = dismissActionComposable,
dismissActionContentColor = dismissActionContentColor,
actionContentColor = actionContentColor,
containerColor = containerColor,
contentColor = contentColor,
shape = shape,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
SnackbarCountdown(
// Calculate the progress of the timer
timerProgress = millisRemaining.toFloat() / totalDuration.toFloat(),
// Calculate the remaining seconds
secondsRemaining = (millisRemaining / 1000) + 1,
color = contentColor
)
// Display the message
Text(snackbarData.visuals.message)
}
}

Congratulations🥳! We’ve successfully built it👏. For the complete code implementation, you can access it on GitHub Gist🧑‍💻. Now, let’s explore how we can put it to use.

Advertisement

Are you learning a foreign language and struggling with new vocabulary? Then, I strongly recommend you check out this words-learning app, which will make your journey easy and convenient!

WordBook

Practical Example 💁

Let’s build a practical example where the user deletes an account and has 5 seconds to undo this crucial action.

Box(Modifier.fillMaxSize()) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
// Define a SnackbarHostState to manage the state of the snackbar
val snackbarHostState = remember { SnackbarHostState() }

Button(
modifier = Modifier.align(Alignment.Center),
onClick = {
scope.launch {
// Show a snackbar
val result = snackbarHostState.showSnackbar(
message = "User account deleted.",
actionLabel = "UNDO",
duration = SnackbarDuration.Indefinite
)
// Handle the snackbar result
when (result) {
SnackbarResult.Dismissed -> {
Toast.makeText(context, "Deleted permanently", Toast.LENGTH_SHORT).show()
}
SnackbarResult.ActionPerformed -> {
Toast.makeText(context, "Deletion canceled", Toast.LENGTH_SHORT).show()
}
}
}
}
) {
Text("Delete Account")
}

// Create a SnackbarHost to display the snackbar
SnackbarHost(
hostState = snackbarHostState,
modifier = Modifier.align(BottomCenter)
) { data ->
// Use the CountdownSnackbar
CountdownSnackbar(data)
}
}

The output:

You might also like 👇

Thank you for reading this article! ❤️ I hope you’ve found it enjoyable and valuable. Feel free to show your appreciation by hitting the clap 👏 if you liked it and follow Kappdev for more exciting articles 😊

Happy coding!

--

--

Kappdev

💡 Curious Explorer 🧭 Kotlin and Compose enthusiast 👨‍💻 Passionate about self-development and growth ❤️‍🔥 Push your boundaries 🚀