Skip to content

Instantly share code, notes, and snippets.

@gab-stargazer
Last active July 22, 2024 08:55
Show Gist options
  • Save gab-stargazer/e66b2bf4d954847ce76164215f9dc531 to your computer and use it in GitHub Desktop.
Save gab-stargazer/e66b2bf4d954847ce76164215f9dc531 to your computer and use it in GitHub Desktop.
This is how to provide a single source for managing snackbar in compose, including on how to make queue system

The Problem in Compose

If you ever use Jetpack Compose, you will probably know how hard it is to use snackbar. Because in Compose, Snackbar require a Host, and a State. Unlike in Android XML, we can just refer any UI part as a snackbar Host, but in Compose, we require specific Component for it so called SnackbarHost which usually placed in a Scaffold Snackbar Slot.

The Solution

In order to solve this problem, we need a way where we can provide Snackbar Host into the Child composable without the need to pass it as a parameter in composable function. Therefore, we're gonna utilize Compose feature, which is Composition Local Provider that was able to provide specific stuff into it's composable Tree, and we're gonna make a custom class to manage this Queue System utilizing Java LinkedList and a Mutex in order to prevent value loss due to concurrency.

The Snackbar Manager

A custom class is required for this in order to make a queue system. Use the code below to achieve the behavior.

import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.lifecycle.AtomicReference
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.LinkedList

class SnackbarManager(
    private val snackbarHostState: SnackbarHostState,
) {

    private val mutex = Mutex()
    private val isRunning: AtomicReference<Boolean> = AtomicReference(initialValue = false)
    private val snackbarQueue = LinkedList<SnackbarData>()

    suspend fun queue(snackbarData: SnackbarData) {
        mutex.withLock {
            snackbarQueue.add(snackbarData)
            if (isRunning.compareAndSet(false, true)) {
                startDequeue()
            }
        }
    }

    private suspend fun startDequeue() {

        val shouldContinue: suspend () -> Unit = {
            if (snackbarQueue.isEmpty()) isRunning.set(false)
            else startDequeue()
        }

        val poppedItem = snackbarQueue.pop()
        val result = snackbarHostState.showSnackbar(
            poppedItem.message,
            poppedItem.actionLabel,
            poppedItem.withDismissAction,
            poppedItem.duration
        )

        when (result) {
            SnackbarResult.Dismissed -> {
                shouldContinue()
            }

            SnackbarResult.ActionPerformed -> {
                shouldContinue()
            }
        }
    }

    data class SnackbarData(
        val message: String,
        val duration: SnackbarDuration = SnackbarDuration.Short,
        val withDismissAction: Boolean = false,
        val actionLabel: String? = null,
    )
}

In order to provide the Snackbar Manager, we need to utilize Composition Local, declare this variable in order to use the manager later. This variable must be able to be accessed by the consumer or user of the Snackbar Manager.

import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf
import com.lelestacia.rt21_sysfor.presenter.util.SnackbarManager

val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState> { error("No Snackbar Host State") }
val LocalSnackbarManager = compositionLocalOf<SnackbarManager> { error("No Snackbar Manager") }

From this, all you need to do is just Set the composition local provider in your theme so it can be accessed from all the composable tree inside your theme

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = false,
    content: @Composable () -> Unit,
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }

        darkTheme -> darkScheme
        else -> lightScheme
    }

    val snackbarHostState = remember {
        SnackbarHostState()
    }

    val snackbarManager = remember {
        SnackbarManager(snackbarHostState)
    }

    CompositionLocalProvider(
        values = arrayOf(
            LocalSnackbarHostState provides snackbarHostState,
            LocalSnackbarManager provides snackbarManager
        )
    ) {
        MaterialTheme(
            colorScheme = colorScheme,
            typography = Typography,
            content = content
        )
    }
}

Now you need to attach the host state into a Snackbar Host for it to be able to work

AppTheme {
  Surface {
    val snackbarHostState = LocalSnackbarHostState.current
     Scaffold(
                        bottomBar = {
                            BottomNavigation(navController)
                        },
                        snackbarHost = {
                            SnackbarHost(snackbarHostState)
                        },
                        modifier = Modifier
                            .fillMaxSize()
                            .statusBarsPadding()
                            .navigationBarsPadding()
                    ) { paddingValues ->
                        NavHost(
                            navController = navController,
                            startDestination = dashboard,
                            modifier = Modifier
                                .fillMaxSize()
                                .padding(paddingValues)
                        ) {
                           // Your content goes here
                        }
                    }
  }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment