Compose: Displaying Snackbars

June 28, 2021

Learn about displaying a Snackbar using Compose

Before We Start

We’ll need to set up our project to use Jetpack Compose (currently you can only use the Compose API with the latest Android Studio Preview). In the app level build.gradle we’ll need to enable compose and also declare its dependencies.

android {
    …

    buildFeatures {
        compose true
    }
    composeOptions {
        // This should be the same as your project’s kotlin version
        kotlinCompilerVersion "1.4.31"
        kotlinCompilerExtensionVersion "1.0.0-beta02"
    }

    …

}

dependencies {

    …

    implementation 'androidx.compose.ui:ui:1.0.0-beta02'

    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.0.0-beta02'

    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation 'androidx.compose.foundation:foundation:1.0.0-beta02'

    // Material Design
    implementation 'androidx.compose.material:material:1.0.0-beta02'

    // Material design icons
    implementation 'androidx.compose.material:material-icons-core:1.0.0-beta02'
    implementation 'androidx.compose.material:material-icons-extended:1.0.0-beta02'

    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.3.0-alpha04'

    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03'

    // Integration with observables
    implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-beta02'

    // Constraint Layout
    implementation "androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha07"
    
    …

}

Try building the project and make sure it succeeds. If there is an error about the compiler version, make sure your com.android.tools.build:gradle is up to date.

Wrong Approach

Implementing a snackbar in compose is not the same as it was in the past. Before we could show a simple snackbar using the following snippet.

Snackbar.make(context, "Some Text", Snackbar.LENGTH_SHORT).show()

In Compose it’s not as short. Compose components behave differently and won’t allow us to do the same thing.

Compose components “recompose” or redraw themselves based on changes of it’s declared states. What this means, in the scope of this article, is that if we wanted to show a snackbar that was in one component, we would have to change its state and have it redraw itself for a snackbar to show.

It may be tempting to use the code snippet below to show a snackbar

val showSnackbar = remember { mutableStateOf(false) }

ConstraintLayout(
    modifier = Modifier.fillMaxSize()
) {
    val button = createRef()
    val snackbar = createRef()

    Button(
        onClick = {
            showSnackbar.value = true
        },
        modifier = Modifier.constrainAs(button) {
            centerTo(parent)
        }
    ) {
        Text(text = "Show Snackbar")
    }

    if (showSnackbar.value) {
        Snackbar(
            modifier = Modifier.constrainAs(snackbar) {
                bottom.linkTo(parent.bottom)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
        ) {
            Text("My Snackbar")
        }
    }
}

Here we declared a new mutable state called showSnackbar. By default the value is false so that it isn’t showing. Clicking “Show Snackbar” will set the value of showSnackbar to true, and cause the component to recompose and show the snackbar. The issue with this approach is that the snackbar will never disappear. We could build some architecture around solving that problem, but there is a built in way of doing that.

Use a SnackbarHost

We can show snackbars using the SnackbarHost provided by Compose. The SnackbarHost is, as its name implies, a host or container for the snackbar to live inside. It will control showing it via a SnackbarHostState object.

val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { mutableStateOf(SnackbarHostState()) }

ConstraintLayout(
    modifier = Modifier.fillMaxSize()
) {
    val button = createRef()
    val snackbar = createRef()

    Button(
        onClick = {
            coroutineScope.launch {
                snackbarHostState.value.showSnackbar(
                    message = "This is a snackbar",
                    duration = SnackbarDuration.Short
                )
            }
        },
        modifier = Modifier.constrainAs(button) {
            centerTo(parent)
        }
    ) {
        Text(text = "Show Snackbar")
    }

    SnackbarHost(
        modifier = Modifier
            .constrainAs(snackbar) {
                bottom.linkTo(parent.bottom)
                start.linkTo(parent.start)
                end.linkTo(parent.end)
            }
            .padding(8.dp),
        hostState = snackbarHostState.value,
        snackbar = {
            Snackbar {
                Text(it.message)
            }
        }
    )
}

This code snippet has a few different things going on that address the issues we saw in the last one. Let’s take a look at it piece by piece.

Remember

val coroutineScope = rememberCoroutineScope()
val snackbarHostState = remember { mutableStateOf(SnackbarHostState()) }

At the top, we declared a coroutine scope and a snackbar host state object. The snackbar host state is used to trigger the snackbar to show using the showSnackbar method it provides. We can only call this method inside a coroutine, which is where coroutineScope comes in!

Button

Button(
    onClick = {
        coroutineScope.launch {
            snackbarHostState.value.showSnackbar(
                message = "This is a snackbar",
                duration = SnackbarDuration.Short
            )
        }
    },
    modifier = Modifier.constrainAs(button) {
        centerTo(parent)
    }
) {
    Text(text = "Show Snackbar")
}

Upon clicking the button, we launch the coroutine and call showSnackbar on snackbarHostState. In this example, we are showing a snackbar without any action, but if that is desired, it can be done using this method. Within the showSnackbar method, we can set the message we want to show and also the duration of the snackbar. The component will redraw itself to show the snackbar, and hide it after the set duration.

SnackbarHost

SnackbarHost(
    modifier = Modifier.constrainAs(snackbar) {
        bottom.linkTo(parent.bottom)
        start.linkTo(parent.start)
        end.linkTo(parent.end)
    }
    .padding(8.dp),
    hostState = snackbarHostState.value,
    snackbar = {
        Snackbar {
            Text(it.message)
        }
    }
)

The interesting part of a SnackbarHost is that it takes in a hostState object and snackbar closure. We can pass in the snackbarHostState we made earlier and then retrieve its data in the snackbar closure where it is SnackbarData. It will only show the snackbar when snackbarHostState.value.showSnackbar is called and will hide it once it has shown the snackbar for the desired duration.

Let's work together

Find out how we can help you grow.