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.
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.
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
}
}
}
}