androidkotlinandroid-jetpack-composeandroid-snackbar

Chain Snackbar calls


I'm trying to use the snackbar on Android Compose. However, as I'm trying to display 2 messages in a row on the snackbar, I can only see the first message, does anybody know what can cause this?

I'm putting the code involved below:

Component code related to Snackbar:

/*
    The state is hoisted. It contains the snackbar message that is not null if
    there is something to display
*/

state.snackbarMessage?.let { resourceId ->
    val message = stringResource(resourceId)
    viewModel.handleEvent(
        AuthenticationEvent.SnackbarMessage(
            message = message,
            type = when {
                state.isFailure -> SnackbarType.ERROR
                else -> SnackbarType.VALIDATION
            }
        )
    )
}

/* Custom Component using Scaffold to host snackbar events */

Frame(
    modifier = modifier,
    snackbarState = viewModel.snackbarState,
    onShowSnackbar = { viewModel.onSnackbarDisplayed() }
) { innerPadding ->
    // UI Content
}

Frame Component:

@Composable
fun Frame(
    modifier: Modifier = Modifier,
    header: @Composable () -> Unit = {},
    snackbarState: StateFlow<MajorSnackbarData?>? = null,
    onShowSnackbar: (() -> Unit)? = null,
    content: @Composable (PaddingValues) -> Unit
) {
    val coroutineScope = rememberCoroutineScope()
    val scaffoldState = rememberScaffoldState()

    val showSnackbar: (MajorSnackbarData) -> Unit = {
        coroutineScope.launch {
            onShowSnackbar?.invoke()

            scaffoldState.snackbarHostState.showSnackbar(
                message = it.message,
                actionLabel = it.type.name,
            )
        }
    }

    snackbarState?.collectAsState()?.value.also { data ->
        if (data != null) {
            showSnackbar(data)
        }
    }

    Scaffold(
        modifier = modifier,
        topBar = header,
        scaffoldState = scaffoldState,
        snackbarHost = {
            SnackbarHost(it) { data ->
                MajorSnackbar(data)
            }
        },
        content = content,
    )
}

ViewModel base that hosts Snackbar:

open class BaseViewModel : ViewModel() {
    private val _snackbarState = MutableStateFlow<MajorSnackbarData?>(null)
    val snackbarState: StateFlow<MajorSnackbarData?> = _snackbarState

    protected fun showSnackBar(
        message: String,
        type: SnackbarType = SnackbarType.ERROR
    ) {
        _snackbarState.value = MajorSnackbarData(message, type)
    }

    open fun onSnackbarDisplayed() {
        _snackbarState.value = null
    }

}

Event handling in a viewmodel:

fun handleEvent(event: Event) {
    when (event) {
        // ...
        is Event.SnackbarMessage -> displayError(event)
    }
}

private fun displayError(event: Event.SnackbarMessage) {
    showSnackBar(event.message, event.type)
}

Solution

  • There is a very simple way of handling snackbars that I always use in my projects. You can directly create a SnackbarHostState in the ViewModel itself and show snackbars directly from the ViewModel.

    // ViewModel
    val snackbarHostState = SnackbarHostState()
    
    fun doSomething() {
        ...
        // If you need to show a snackbar here, directly call the showSnackbar function
        snackbarHostState.showSnackbar(
            message = // Your message,
            actionLabel = // Your action label,
        )
    }
    
    // In your composable
    val scaffoldState = rememberScaffoldState(snackbarHostState = viewModel.snackbarHostState)
    
    Scaffold(
        modifier = modifier,
        topBar = header,
        scaffoldState = scaffoldState,
        snackbarHost = {
            SnackbarHost(it) { data ->
                MajorSnackbar(data)
            }
        },
        content = content,
    )
    

    This will give you a lot more control over the snackbars. If you need to display two snackbars one after the other, just place the two calls together in the viewModel.

    snackbarHostState.showSnackbar(
        message = message1,
        actionLabel = actionLabel1,
    )
    snackbarHostState.showSnackbar(
        message = message2,
        actionLabel = actionLabel2,
    )
    

    Note that because showSnackbar is a suspend function which suspends till the snackbar is visible, the second snackbar will appear only after the first one has gone.