Tonnie
Tonnie

Reputation: 8112

Show Snackbar in Material Design 3 using Scaffold

Decided to try out Material Design 3 in my new Jetpack Compose project. Everything was cozy until I needed to show a Snackbar when I hit a wall.

In MD2 this was super-easy and you would show the snackbar in a Scaffold done with the SnackbarHostState.showSnackbar() function inside a Coroutine scope. I observed you only needed to import androidx.compose.material.rememberScaffoldState from Material Library.

import androidx.compose.material.rememberScaffoldState


@Composable
fun MyScreenInMaterial2() {
    val scaffoldState = rememberScaffoldState()
}

When I try the same in MD3 the rememberScaffoldState() function is not being resolved. enter image description here

For those who have dived in MD3 world how do you show a Snackbar in a Scaffold? I have checked the docs and online resources but I haven't come across a solution.

Upvotes: 19

Views: 12088

Answers (3)

mochadwi
mochadwi

Reputation: 1268

In my case, there's compile error with:

@Composable invocations can only happen from the context of a @Composable function

When calling the LaunchedEffect inside the onClickChip lambda.

There's an alternative options, inspired from this answer

For those wants to show the Compose Snackbar inside a lambda (w/o @Composable)

@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
@Composable
fun ScreenA() {
    val snackbarHostState = remember { SnackbarHostState() }
    val snackbarMessage = "Succeed!"
    val showSnackbar = remember { mutableStateOf(false) }

    LaunchedEffect(showSnackbar.value) {
        if (showSnackbar.value)
            snackbarHostState.showSnackbar(snackbarMessage)
    }
    Surface {
        Scaffold(
            snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
        ) {
            ChipIcon(
                text = "Click me: ${showSnackbar.value}",
                onClickChip = {
                    // unable to use LaunchedEffect in here
                    showSnackbar.value = !showSnackbar.value
                }
            )
        }
    }
}

// Custom Composable
@Composable
fun ChipIcon(
    text: String,
    onClickChip: () -> Unit, // no @Composable, unable to use LaunchedEffect in this lambda
) {
    Row(
        modifier = Modifier
            .clickable(
                enabled = true,
                onClick = {
                    onClickChip()
                }
            ),
    ) {
        Text(text = text)
    }
}

Observe the showSnackbar.value changes instead on LaunchedEffect for cancle/re-launch, then show snackbar if the showSnackbar.value == true, otherwise do nothing

Upvotes: 4

Joaquin Iurchuk
Joaquin Iurchuk

Reputation: 5617

Bsed on Damian Petla's answer.

If you use a CoroutineScope then you'll get a warning saying CoroutineCreationDuringComposition.

Official documentation suggests using LaunchedEffect instead to avoid this side-effect.

So the suggested code would look like this:

val snackbarHostState = remember { SnackbarHostState() }

Scaffold(
    snackbarHost = { SnackbarHost(snackbarHostState) },
    floatingActionButton = {
        var clickCount by remember { mutableStateOf(0) }
        ExtendedFloatingActionButton(
            onClick = {
                LaunchedEffect(snackbarHostState) {
                    snackbarHostState.showSnackbar(
                        "Snackbar # ${++clickCount}"
                    )
                }
            }
        ) { Text("Show snackbar") }
    },
    content = { innerPadding ->
        Text(
            text = "Body content",
            modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
        )
    }
)

Upvotes: 8

Damian Petla
Damian Petla

Reputation: 9103

Here you have an example from the official documentation.

val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
    snackbarHost = { SnackbarHost(snackbarHostState) },
    floatingActionButton = {
        var clickCount by remember { mutableStateOf(0) }
        ExtendedFloatingActionButton(
            onClick = {
                // show snackbar as a suspend function
                scope.launch {
                    snackbarHostState.showSnackbar(
                        "Snackbar # ${++clickCount}"
                    )
                }
            }
        ) { Text("Show snackbar") }
    },
    content = { innerPadding ->
        Text(
            text = "Body content",
            modifier = Modifier.padding(innerPadding).fillMaxSize().wrapContentSize()
        )
    }
)

Upvotes: 31

Related Questions