Blog Infos
Author
Published
Topics
Published

Introduction

A few days ago I received the task — display the Snackbar in case of the API call is finished with the error, like InternalServiceErrorBadGateway or NotFound.

As I didn’t work with the Snackbar before in Jetpack Compose, I started to search the best practices of its implementation. The most helpful tutorial was this as it fully described how to show a Snackbar.

Now it’s time to describe the architecture of the app.

UI-side arch looks like this:

  • I use single activity architecture (since it is Compose it’s easy to follow this principle)
  • My Compose app looks like this
fun FitnestApp() {
val navController = rememberAnimatedNavController(AnimatedComposeNavigator())
FitnestTheme {
Scaffold(
bottomBar = { BottomBar(navController) },
topBar = { TopBar(navController) },
) {
AnimatedNavHost(
navController = navController,
startDestination = Route.Splash.screenName
) {
composable(route = Route.Splash.screenName) {
SplashScreen(navController = navController)
}
...
}
}
}
}
view raw FitnestApp.kt hosted with ❤ by GitHub
  • Screens are displayed in NavHost, here’s an example of the screen, which can receive the server error
@Composable
fun SplashScreen(navController: NavController) {
...
LaunchedEffect(key1 = null) {
launch {
viewModel.failureSharedFlow.collect {
// TODO - show snackbar with error message
}
}
}
Box {
...
}
}
view raw SplashScreen.kt hosted with ❤ by GitHub

As you can see, the error which I should show to the user is coming from the ViewModel via SharedFlow.

Implementation

To show the Snackbar we should set the ScaffoldState to the Scaffold. ScaffoldState contains 2 fields — drawerState (not interesting for us now) and snackbarHostState. As it is said in documentation:

SnackbarHostState — State of the SnackbarHost, controls the queue and the current Snackbar being shown inside the SnackbarHost.

In code it looks like this:

@Composable
fun SnackbarDemo() {
val scaffoldState: ScaffoldState = rememberScaffoldState()
Scaffold(scaffoldState = scaffoldState) {
Button(onClick = {
scaffoldState.snackbarHostState.showSnackbar(
message = "This is your message",
actionLabel = "Do something"
)
}) {
Text(text = "Click me!")
}
}
}

And now we face the first problem — our screen (i.e. SplashScreen) doesn’t know anything about Scaffold’s state, since the Scaffold is placed in the root of the App, not at the screen level.

First solution, which came into my mind is to pass the state as a param to all Composable methods which should handle the exception. But this solution doesn’t seem to be perfect, because we have to pass state to almost all composables and sometimes we need to pass it not because our function need it, but because some inner function need it.

The second solution was to define a global variable, which will keep the state, and composables, which need to show a Snackbar will access it. I used this approach, but a bit modified it.

Here its code:

internal class SnackbarDelegate(
var snackbarHostState: SnackbarHostState? = null,
var coroutineScope: CoroutineScope? = null
) {
fun showSnackbar(
message: String,
actionLabel: String? = null,
duration: SnackbarDuration = SnackbarDuration.Short
) {
coroutineScope?.launch {
snackbarHostState?.showSnackbar(message, actionLabel, duration)
}
}
}

As you can see, SnackbarDelegate is just a wrapper over SnackbarHostState and CoroutineScope (we need it to show a Snackbar, because showSnackbar is a suspend function). Object of this class is registered as singleton in DI container, so all the classes and methods, which will have an access to this object will work with the same SnackbarHostState (the state of the SnackbarHost of the root Scaffold. You can see it in the code snippet below).

fun FitnestApp() {
val snackbarDelegate: SnackbarDelegate by rememberInstance()
val scaffoldState = rememberScaffoldState()
snackbarDelegate.apply {
snackbarHostState = scaffoldState.snackbarHostState
coroutineScope = rememberCoroutineScope()
}
FitnestTheme {
Scaffold(
scaffoldState = scaffoldState,
) {
}
}
}
view raw app.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

, ,

Migrating to Jetpack Compose – an interop love story

Most of you are familiar with Jetpack Compose and its benefits. If you’re able to start anew and create a Compose-only app, you’re on the right track. But this talk might not be for you…
Watch Video

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer for Jetpack Compose
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engin ...
Google

Migrating to Jetpack Compose - an interop love story

Simona Milanovic
Android DevRel Engineer f ...
Google

Jobs

And now we face the next problem — different states of Snackbar. For example, in my App there are 2 states — Error and Default Snackbar. Error Snackbar should have Red background, while Default snackbar should have a Blue color. Scaffold provides us an option to customise a snackbar by a snackbarHost param. It allows us to display any Composable as a Snackbar, but it doesn’t know anything about our internal Snackbar states. I solved this problem with the help of SnackbarDelegate. Here’s it modified version.

enum class SnackbarState {
DEFAULT,
ERROR
}
internal class SnackbarDelegate(
var snackbarHostState: SnackbarHostState? = null,
var coroutineScope: CoroutineScope? = null
) {
private var snackbarState: SnackbarState = SnackbarState.DEFAULT
val snackbarBackgroundColor: Color
@Composable
get() = when (snackbarState) {
SnackbarState.DEFAULT -> SnackbarDefaults.backgroundColor
SnackbarState.ERROR -> ErrorColor
}
fun showSnackbar(
state: SnackbarState,
message: String,
actionLabel: String? = null,
duration: SnackbarDuration = SnackbarDuration.Short
) {
this.snackbarState = state
coroutineScope?.launch {
snackbarHostState?.showSnackbar(message, actionLabel, duration)
}
}
}

There’re several differences between this and previous version:

  1. I defined a variable, which will keep current SnackbarState (my App’s internal state)
  2. Before the showing of Snackbar I save this state (line 26)
  3. Provided a getter for a Snackbar’s background.

And that’s the way how it is used in the App’s Composable

fun FitnestApp() {
val snackbarDelegate: SnackbarDelegate by rememberInstance()
val scaffoldState = rememberScaffoldState()
snackbarDelegate.apply {
snackbarHostState = scaffoldState.snackbarHostState
coroutineScope = rememberCoroutineScope()
}
FitnestTheme {
Scaffold(
scaffoldState = scaffoldState,
snackbarHost = {
SnackbarHost(hostState = it) {
val backgroundColor = snackbarDelegate.snackbarBackgroundColor
Snackbar(snackbarData = it, backgroundColor = backgroundColor)
}
}
)
}
}
view raw app_full.kt hosted with ❤ by GitHub

To use the delegate you should inject the delegate to the composable and call showSnackbar method. Here’s an example:

fun SplashScreen(navController: NavController) {
val viewModelFactory: ViewModelProvider.Factory by rememberInstance()
val errorHandlerDelegate: ErrorHandlerDelegate by rememberInstance()
val viewModel = viewModel(
factory = viewModelFactory,
modelClass = SplashViewModel::class.java
)
LaunchedEffect(key1 = null) {
launch {
viewModel.failureSharedFlow.collect(errorHandlerDelegate::defaultHandleFailure)
}
}
Box {}
}
internal class ErrorHandlerDelegate(
private val context: Context,
private val snackbarService: SnackbarDelegate
) {
fun defaultHandleFailure(failure: Failure) {
when (failure) {
is Failure.ServerError -> snackbarService.showSnackbar(
SnackbarState.ERROR,
context.getString(R.string.error_server_error)
)
else -> {}
}
}
}
view raw splash.kt hosted with ❤ by GitHub

In my case I added a mediator — ErrorHandlerDelegate. It allows me to handle errors in the simple way all over the application.

This solution is scalable, so if I will need to specify some other params in Snackbar, I can put them to the delegate class and will handle their logic inside the class.

And that’s it!

Conclusion

In this article I described a solution, which allows to work with the Snackbar in the big Jetpack Compose app and handle its different states.

You can find the source code at the Github.

Thank you for reading! Feel free to ask questions and leave the feedback in comments or Linkedin.

This article was originally published on proandroiddev.com on October 9, 2022

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu