Blog Infos
Author
Published
Topics
Published

One of the most common use case we come across while developing mobile applications is showing a SnackBar to the end user. It is not uncommon that the design/product folks come up with a fancy SnackBar to be shown to the users for enhanced user experience.

I came across a similar requirement, showing a snackbar with multiple small images and custom text. But does Jetpack Compose support passing custom data (more than just a text) to SnackBar? Spoiler alert, it does not! Do not worry, I got you covered! In this article, we will see what is the default behavior of the SnackBar in Jetpack Compose and how I created a custom and generic SnackBar implementation to solve the problem/use case I just mentioned above. Let’s get started with code directly!

Default SnackBar Implementation

As I mentioned before, the default snackbar in compose, only accepts a string param as data, which you can pass while calling the function showSnackbar(message) from your SnackbarHost. Without getting into much details about what a SnackBar, SnackBarHost and SnackbarHostState is, let’s check the code, which is pretty self-explanatory.

@Composable
fun SimpleSnackBar(text: String) {
    val hostState = remember { SnackbarHostState() }
    
    LaunchedEffect(key1 = Unit) {
        hostState.showSnackbar(message = text)
    }

    SnackbarHost(hostState = hostState) { snackbarData : String ->
        Snackbar {
            Text(text = snackbarData.message)
        }
    }
}

Now you may ask, what are the fields and functions of SnackBarData and why can’t we simply modify it? Let’s check the internal implementation of SnackBarHost which returns us the SnackBarData param and SnackBarHostState method showMessage, which accepts a string param.

@Stable
class SnackbarHostState {

    private val mutex = Mutex()

    var currentSnackbarData by mutableStateOf<SnackbarData?>(null)
        private set


    suspend fun showSnackbar(
        message: String,
        actionLabel: String? = null,
        duration: SnackbarDuration = SnackbarDuration.Short
    ): SnackbarResult = mutex.withLock {
        try {
            return suspendCancellableCoroutine { continuation ->
                currentSnackbarData = SnackbarDataImpl(message, actionLabel, duration, continuation)
            }
        } finally {
            currentSnackbarData = null
        }
    }

    @Stable
    private class SnackbarDataImpl(
        override val message: String,
        override val actionLabel: String?,
        override val duration: SnackbarDuration,
        private val continuation: CancellableContinuation<SnackbarResult>
    ) : SnackbarData {

        override fun performAction() {
            if (continuation.isActive) continuation.resume(SnackbarResult.ActionPerformed)
        }

        override fun dismiss() {
            if (continuation.isActive) continuation.resume(SnackbarResult.Dismissed)
        }
    }
}
interface SnackbarData {
    val message: String
    val actionLabel: String?
    val duration: SnackbarDuration

    /**
     * Function to be called when Snackbar action has been performed to notify the listeners
     */
    fun performAction()

    /**
     * Function to be called when Snackbar is dismissed either by timeout or by the user
     */
    fun dismiss()
}

As you can see, the method showMessage of the SnackBarHost, only accepts a string param, which in turn in passed to an internal implementation of SnackBarData, so there is no scope of playing around with the same or in other words, replacing the string in SnackBarData with a custom object that fits our need. So how do we solve the problem?

Modifier SnackBar Implementation

It is clear till now that we cannot play around with public methods or fields of the SnackBarHost or SnackBarHostState to pass custom data to SnackBarHostState and eventually receive the same in SnackBarHost, which is where we finally show our SnackBar. So I decided to create a custom implementation of the SnackBar with generic param in SnackBar, so I can have the flexibility of getting my data as a custom object instead of a string.

Let us check our modified SnackBarHostState and SnackBarData

@Stable
class GenericSnackbarHostState<T> {

    private val mutex = Mutex()

    var currentSnackbarData by mutableStateOf<GenericSnackbarData<T>?>(null)
        private set


    suspend fun showSnackbar(
        message: T,
        actionLabel: String? = null,
        duration: GenericSnackbarDuration = GenericSnackbarDuration.Short
    ): SnackbarResult = mutex.withLock {
        try {
            return suspendCancellableCoroutine { continuation ->
                currentSnackbarData = GenericSnackbarDataImpl(message, actionLabel, duration, continuation)
            }
        } finally {
            currentSnackbarData = null
        }
    }


    @Stable
    private class GenericSnackbarDataImpl<T>(
        override val message: T,
        override val actionLabel: String?,
        override val duration: GenericSnackbarDuration,
        private val continuation: CancellableContinuation<SnackbarResult>
    ) : GenericSnackbarData<T> {

        override fun performAction() {
            if (continuation.isActive) continuation.resume(SnackbarResult.ActionPerformed)
        }

        override fun dismiss() {
            if (continuation.isActive) continuation.resume(SnackbarResult.Dismissed)
        }
    }
}
interface GenericSnackbarData<Param> {
    val message: Param
    val actionLabel: String?
    val duration: GenericSnackbarDuration

    fun performAction()

    fun dismiss()
}

As you can see, now in GenericSnackbarHostState, I have add a generic param T, which would require the developer to provide the type of data (like a custom data class) which should be used inside SnackBarData at the time of SnackBarHost class declaration itself. You can see the difference between the declaration of SnackBarHost vs GenericSnackBarHost below.

val snackBarHostState = remember { SnackbarHostState() }

val genericSnackBarHostState = remember { GenericSnackbarHostState<CustomClass>() }

So to show a snackbar with this custom data, we need to also modify the SnackBarHost, so instead of accepting the default SnackBarHostState, it can accept our GenericSnackBarHostState, which we declared earlier, with our custom defined class type.

@Composable
fun <T> GenericSnackBarHost(
    hostState: GenericSnackbarHostState<T>,
    modifier: Modifier = Modifier,
    snackbar: @Composable (GenericSnackbarData<T>) -> Unit
) {
    val currentSnackbarData = hostState.currentSnackbarData
    val accessibilityManager = LocalAccessibilityManager.current
    LaunchedEffect(currentSnackbarData) {
        if (currentSnackbarData != null) {
            val duration = currentSnackbarData.duration.toMillis(
                currentSnackbarData.actionLabel != null,
                accessibilityManager
            )
            delay(duration)
            currentSnackbarData.dismiss()
        }
    }
    FadeInFadeOutWithScale(
        current = hostState.currentSnackbarData,
        modifier = modifier,
        content = snackbar
    )
}

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

No results found.

While implementing this GenericSnackBarHost, you will notice that the GenericSnackbarData of this SnackBar is same as the custom class type, which you defined earlier while declaring GenericSnackbarHostState, hence solving our problem. I have attached the code for GenericSnackBarHost

Here is the link to the code of GenericSnackHostState and other relevant classes/interface related to the same below, so you can also use the same.

End Result

Enough talk and code, let’s see a live example of what I achieved using this generic implementation. I have passed 3 strings and 1 lambda, yes a function inside my SnackBar using the above implementation.

@Composable
fun PokemonSnackBar(
    pokemonObject: PokemonObject
) {
    val hostState = remember { GenericSnackbarHostState<PokemonObject>() }

    LaunchedEffect(key1 = Unit) {
        hostState.showSnackbar(message = pokemonObject)
    }

    GenericSnackBarHost(hostState = hostState) { pokemonObject ->
        ConstraintLayout(modifier = Modifier
            .padding(vertical = 50.dp)
            .height(48.dp)
            .padding(horizontal = 8.dp)
            .background(
                color = Color(0xBF171717), shape = RoundedCornerShape(8.dp)
            )
            .clip(CircleShape)
            .fillMaxWidth(), constraintSet = ConstraintSet {
            val pokemonImage = createRefFor("pokemonImage")
            val buyPokemonText = createRefFor("buyPokemonText")
            val rightArrow = createRefFor("rightArrow")
            val buyPokemonCTA = createRefFor("buyPokemonCTA")


            constrain(pokemonImage) {
                top.linkTo(parent.top)
                bottom.linkTo(parent.bottom)
                start.linkTo(parent.start)
            }

            constrain(buyPokemonText) {
                top.linkTo(parent.top)
                bottom.linkTo(parent.bottom)
                start.linkTo(pokemonImage.end)
                width = Dimension.fillToConstraints
            }


            constrain(buyPokemonCTA) {
                linkTo(
                    start = buyPokemonText.start,
                    top = parent.top,
                    end = parent.end,
                    bottom = parent.bottom,
                    horizontalBias = 1.0f
                )
            }

            constrain(rightArrow) {
                linkTo(
                    start = buyPokemonCTA.start,
                    top = buyPokemonCTA.top,
                    end = buyPokemonCTA.end,
                    bottom = buyPokemonCTA.bottom,
                )
            }
        }) {


            Image(
                modifier = Modifier
                    .padding(horizontal = 12.dp, vertical = 8.dp)
                    .size(32.dp)
                    .layoutId("pokemonImage"),
                painter = rememberAsyncImagePainter(model = pokemonObject.message.pokemonImage),
                contentDescription = ""
            )


            Text(
                modifier = Modifier.layoutId("buyPokemonText"),
                text = "${pokemonObject.message.text} ${pokemonObject.message.price}",
                fontSize = 15.sp,
                color = Color(0xFFFFFFFF)
            )


            Box(
                modifier = Modifier
                    .layoutId("buyPokemonCTA")
                    .padding(horizontal = 12.dp)
                    .size(32.dp)
                    .background(
                        Color(0x33FFFFFF), shape = CircleShape
                    )
                    .clip(shape = CircleShape)
                    .clickable {
                        pokemonObject.message.action.invoke()
                    }, contentAlignment = Alignment.Center
            ) {
                Image(
                    modifier = Modifier.layoutId("rightArrow"),
                    painter = painterResource(id = R.drawable.chevron_right),
                    contentDescription = ""
                )
            }
        }
    }
}

 

One of the most common use case we come across while developing mobile applications is showing a SnackBar to the end user. It is not uncommon that the design/product folks come up with a fancy SnackBar to be shown to the users for enhanced user experience.

Click here for code link

Please like/upvote the article if you link it, and do let me know in the comments if you found that useful. Thanks for reading, and I’ll see you in my next article!

This article was previously published on proandroiddev.com

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