mike so
mike so

Reputation: 417

How to show snackbar with a button onclick in Jetpack Compose

I want to show snackbar with a button onclick in Jetpack Compose
I tried this

Button(onClick = {
    Snackbar(action = {}) {
        Text("hello")
    }
} 

But AS said "@Composable invocations can only happen from the context of a @Composable function"
Shall you give me a nice program.
I wish it can run in Button.onclick()

Upvotes: 38

Views: 32393

Answers (9)

android_dev71
android_dev71

Reputation: 617

Just in case you need a snackbar as a separate function to call in other screens with custom parameters, and without using scaffold, I use this below.

UPDATE September 2024

In a recent project, I've rewrite my old solution (below) with more customization and updating for compose bom version 2024.09.01:

@SuppressLint("RestrictedApi")
fun SnackbarAlert(
    message: String,
    showSb: Boolean,
    backgroundColorIn: Color,
    messageColorIn: Color,
    openSnackbar: (Boolean) -> Unit,
    snackbarMsg: (String) -> Unit,
) {
    val snackState = remember { SnackbarHostState() }
    val snackScope = rememberCoroutineScope()

    SnackbarHost(
        modifier = Modifier
            .fillMaxSize()
            .wrapContentHeight(Alignment.Bottom),
        hostState = snackState,
        snackbar = { data ->
            MessageCardForSnackbar(
                snackbarData = data,
                backgroundColor = backgroundColorIn,
                strokeColor = backgroundColorIn,
                icon = painterResource(id = R.drawable.ic_xmark_white),
                messageColor = messageColorIn
            )
        }
    )

    if (showSb) {
        LaunchedEffect(Unit) {
            snackScope.launch {
                snackState.showSnackbar(
                    message = message,
                    // actionLabel = "OK",
                    duration = SnackbarDuration.Short
                )
                openSnackbar(false)
            }
            snackbarMsg(" ")
        }
    }
}


// customized content
@Composable
fun MessageCardForSnackbar(
    modifier: Modifier = Modifier,
    snackbarData: SnackbarData,
    cornerRadius: Dp = 10.dp,
    backgroundColor: Color = Color.White,
    strokeWidth: Dp = 2.dp,
    strokeColor: Color = Color.LightGray,
    icon: Painter,
    iconPadding: Dp = 16.dp,
    messageColor: Color = Color.Black
) {
    Card(
        modifier = modifier,
        shape = RoundedCornerShape(cornerRadius),
        colors = CardDefaults.cardColors(containerColor = backgroundColor),
        border = BorderStroke(
            width = strokeWidth,
            color = strokeColor
        )
    ) {
        ConstraintLayout(
            modifier = Modifier
                .fillMaxWidth()
                .padding(8.dp)
        ) {
            val (iconRef, textRef) = createRefs()

            Image(
                painter = icon,
                contentDescription = null,
                modifier = Modifier
                    .constrainAs(iconRef) {
                        start.linkTo(parent.start)
                        top.linkTo(parent.top)
                        bottom.linkTo(parent.bottom)
                    }
                    .padding(iconPadding)
            )

            Text(
                text = snackbarData.visuals.message,
                style = text_16(messageColor, false),
                modifier = Modifier
                    .constrainAs(textRef) {
                        start.linkTo(iconRef.end, 16.dp)
                        end.linkTo(parent.end, margin = 16.dp)
                        top.linkTo(parent.top, margin = 16.dp)
                        bottom.linkTo(parent.bottom, margin = 16.dp)
                        width = Dimension.fillToConstraints
                    }
            )
        }
    }
}

@Preview(showBackground = true)
@Composable
fun SnackbarAlert_Preview_01() {

    var showSnackbar by remember { mutableStateOf(false) }
    var snackbarMessage by remember { mutableStateOf("") }
    var snackbarBackgroundColorIn  by remember { mutableStateOf(Color.Blue) }
    var snackbarMessageColorIn  by remember { mutableStateOf(Color.White) }

    SnackbarAlert(
        message = snackbarMessage,
        showSb = showSnackbar,
        backgroundColorIn = snackbarBackgroundColorIn,
        messageColorIn = snackbarMessageColorIn,
        openSnackbar = { showSnackbar = it },
        snackbarMsg = { snackbarMessage = it }
    )

    Button(onClick = {
        showSnackbar = true
        snackbarBackgroundColorIn = Light_ColorAlert
        snackbarMessageColorIn = Light_PrimaryColor
        snackbarMessage = "Email already used."
    }) {
        Text("Show Snackbar")
    }
}

in build.gradle module file :

def composeBom = libs.androidx.compose.bom
implementation(composeBom)
androidTestImplementation(composeBom)
implementation libs.androidx.runtime
implementation libs.androidx.ui
implementation libs.androidx.foundation
implementation libs.androidx.foundation.layout
implementation libs.androidx.material
implementation libs.androidx.material3
implementation libs.androidx.material.icons.extended
implementation libs.androidx.runtime.livedata
implementation libs.androidx.ui.tooling
implementation libs.androidx.ui.tooling.preview
implementation libs.androidx.navigation.compose
implementation libs.androidx.constraintlayout.compose
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2"

in libs.versions.toml

[versions]
    composeBom = "2024.09.01"

    [libraries]
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }

In my opinion is quite a lot of code just to show a snackbar in jetpack compose, customization apart...

So naively I've write this, if can help someone: it doesn't use snackbar, and simple show the message card above modified so that it'll be dismissed after a timerCount millisec or by touch :

@Composable
fun MessageCardDismissable(
    modifier: Modifier = Modifier,
    showCard: Boolean,             // Control visibility
    timerCount: Long = 4000,       // Time Duration before dismissing
    cornerRadius: Dp = 10.dp,
    backgroundColor: Color = Color.White,
    strokeWidth: Dp = 2.dp,
    strokeColor: Color = Color.LightGray,
    icon: Painter,
    iconPadding: Dp = 16.dp,
    message: String,
    messageColor: Color = Color.Black,
    onDismiss: () -> Unit = {}      // Callback when dismissed if needed
) {
    var visible by remember { mutableStateOf(showCard) } // Local state for visibility

    
    LaunchedEffect(key1 = visible) {
        if (visible) {
            delay(timerCount)
            visible = false
            onDismiss()
        }
    }

    if (visible) {
        Card(
            modifier = modifier
                .clickable {            
                    visible = false
                    onDismiss()
                },
            shape = RoundedCornerShape(cornerRadius),
            colors = CardDefaults.cardColors(containerColor = backgroundColor),
            border = BorderStroke(
                width = strokeWidth,
                color = strokeColor
            )
        ) {
            ConstraintLayout(
                modifier = Modifier
                    .fillMaxWidth()
                    .padding(8.dp)

            ) {
                val (iconRef, textRef) = createRefs()

                Image(
                    painter = icon,
                    contentDescription = null,
                    modifier = Modifier
                        .constrainAs(iconRef) {
                            start.linkTo(parent.start)
                            top.linkTo(parent.top)
                            bottom.linkTo(parent.bottom)
                        }
                        .padding(iconPadding)
                )


                Text(
                    text = message,
                    style = text_16(messageColor, false),
                    modifier = Modifier
                        .constrainAs(textRef) {
                            start.linkTo(iconRef.end, 16.dp)
                            end.linkTo(parent.end, margin = 16.dp)
                            top.linkTo(parent.top, margin = 16.dp)
                            bottom.linkTo(parent.bottom, margin = 16.dp)
                            width = fillToConstraints
                        }
                )
            }
        }
    }
}



@Preview(showBackground = true)
@Composable
fun MessageCardPreview_01() {
    var showCard by remember { mutableStateOf(true) }

    MessageCardDismissable(
        showCard = showCard,
        icon = painterResource(id = R.drawable.ic_xmark_white),
        messageColor = Color.White,
        backgroundColor = Color.Red,
        message = "This is an ERROR message card!"
    ) {
        showCard = false // Reset after dismissing
    }
}

@Preview(showBackground = true)
@Composable
fun MessageCard_WithContainer_Preview() {
    var showCard by remember { mutableStateOf(false) }

    Column {
        Button(onClick = { showCard = true }) {
            Text("Show Card")
        }

        if (showCard){
            MessageCardDismissable(
                showCard = showCard,
                icon = painterResource(id = R.drawable.ic_xmark_white),
                messageColor = Color.White,
                backgroundColor = Color.Red,
                message = "This is an ERROR message card!"
            ) {
                showCard = false
            }
        }
    }
}

PREVIOUS SOLUTION

The parameter openSnackbar: (Boolean) -> Unit is used to reset the opening condition var openMySnackbar by remember { mutableStateOf(false) } and allow open snackbar other times.

    @Composable
    fun SnackbarWithoutScaffold(
        message: String,
        showSb: Boolean,
        openSnackbar: (Boolean) -> Unit
    ) {
    
        val snackState = remember { SnackbarHostState() }
        val snackScope = rememberCoroutineScope()
    
        SnackbarHost(
            modifier = Modifier,
            hostState = snackState
        ){
            Snackbar(
                snackbarData = it,
                backgroundColor = Color.White,
                contentColor = Color.Blue
            )
        }
    
    
        if (showSb){
            LaunchedEffect(Unit) {
                snackScope.launch { snackState.showSnackbar(message) }
                openSnackbar(false)
            }
   
        }
    
    
    }

and I've used like this :

@Composable fun MyScreen() {

var openMySnackbar by remember { mutableStateOf(false)  }
var snackBarMessage by remember { mutableStateOf("") }

Column() {

    Button(
        shape = RoundedCornerShape(50.dp),
        onClick = {
            openMySnackbar = true
            snackBarMessage = "Your message"
        },
    ) {
        Text(text = "Open Snackbar")
    }

    SnackbarWithoutScaffold(snackBarMessage, openMySnackbar, , {openMySnackbar = it})
}

}

Upvotes: 7

Farman Ali Khan
Farman Ali Khan

Reputation: 896

Snackbar in Material3 with action

@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun DemoSnackBar() {
    val snackBarHostState = remember {
        SnackbarHostState()
    }
    val coroutineScope = rememberCoroutineScope()
    Scaffold(content = {
        Column(
            modifier = Modifier.fillMaxSize(),
            verticalArrangement = Arrangement.Center,
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            Button(onClick = {
                coroutineScope.launch {

                    val snackBarResult = snackBarHostState.showSnackbar(
                        message = "Snackbar is here",
                        actionLabel = "Undo",
                        duration = SnackbarDuration.Short
                    )
                    when (snackBarResult) {
                        SnackbarResult.ActionPerformed -> {
                            Log.d("Snackbar", "Action Performed")
                        }
                        else -> {
                            Log.d("Snackbar", "Snackbar dismissed")
                        }
                    }
                }

            }) {
                Text(text = "Show Snack Bar", color = Color.White)
            }
        }
    }, snackbarHost = { SnackbarHost(hostState = snackBarHostState) })
}

Upvotes: 9

Thracian
Thracian

Reputation: 66674

You can show snackBar without Scaffold using SnackBar Composable either.

Box(modifier = Modifier.fillMaxSize()) {
    var showSnackbar by remember {
        mutableStateOf(false)
    }

    LaunchedEffect(key1 = showSnackbar) {
        if (showSnackbar) {
            delay(2000)
            showSnackbar = false
        }
    }


    Column {

        Button(onClick = {
            showSnackbar = !showSnackbar
        }) {
            Text("Show Snackbar")
        }
    }

    if (showSnackbar) {
        androidx.compose.material.Snackbar(modifier = Modifier.align(Alignment.BottomStart),
            action = {
                Text(text = "Action",
                    color = Color(0xffCE93D8),
                    modifier = Modifier.clickable {
                        showSnackbar = false
                    }
                )
            }) {
            androidx.compose.material.Text("Message")
        }
    }
}

LaunchedEffect is to remove it from composition. You can use a slide animation if you want to.

Upvotes: 2

Carmen
Carmen

Reputation: 6263

First of all you need a SnackbarHostState, you can pass this state down to your composable where you want to trigger a snackbar message. or if you use a scaffold use that one scaffoldState.snackbarHostState

val snackbarHostState = remember { SnackbarHostState() }

Showing a snackbar is a side effect and should be wrapped in a LaunchEffect Composable

@Composable
fun MyScreen(
    scaffoldState: ScaffoldState = rememberScaffoldState()
) {
   Scaffold(scaffoldState = scaffoldState) {    

     Button(onClick = {
      LaunchedEffect(scaffoldState.snackbarHostState) {
         scaffoldState.snackbarHostState.showSnackbar(
                  message = "hello world",
                  actionLabel = "Retry"
         )
      }

    
     // ...
    }
}

See more information here https://developer.android.com/topic/architecture/ui-layer/events#consuming-trigger-updates

and https://developer.android.com/jetpack/compose/side-effects

Upvotes: 2

Z3nk
Z3nk

Reputation: 395

Let me add an other answer, i was not using scaffold and was using state hoisting

val loginState by viewModel.uiState.collectAsState() // STATE HOISTING
val snackbarHostState = remember { SnackbarHostState() }
val message = stringResource(id = R.string.error_login) 
if (loginState.snackbarVisible) { // When passing true, show snackbar
    LaunchedEffect(snackbarHostState) {
        snackbarHostState.showSnackbar(
            message = message,
            actionLabel = "Do something."
        )
    }
}


Box(
    modifier = Modifier.fillMaxSize()
) {
    // Your screen content
    // ...
    SnackbarHost(
        hostState = snackbarHostState,
        modifier = Modifier.align(Alignment.BottomCenter)
    ) // Snackbar location
}

Upvotes: 1

brucemax
brucemax

Reputation: 962

If you use new Material3 there is new field in Scaffold: "snackbarHost"

val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()

Scaffold(
  snackbarHost = { SnackbarHost(snackbarHostState) },
  ...

            // for showing snackbar in onClick for example:
            scope.launch {
                snackbarHostState.showSnackbar(
                    "Snackbar Test"
                )
            }

Upvotes: 22

jendress
jendress

Reputation: 169

Building on Bartek's answer you can also use Compose's side-effects. Then you don't have to manage the CoroutineScope itself.

Since Composables itself should be side-effect free, it is recommended to make use of Compose's Effect APIs so that those side effects are executed in a predictable manner.

In your specific case you can use the LaunchedEffect API to run suspend functions in the scope of a composable. The example code would look like the following:

@Composable
fun SnackbarDemo() {
    val scaffoldState = rememberScaffoldState() // this contains the `SnackbarHostState`
    val (showSnackBar, setShowSnackBar) = remember {
        mutableStateOf(false)
    }
    if (showSnackBar) {
        LaunchedEffect(scaffoldState.snackbarHostState) {
            // Show snackbar using a coroutine, when the coroutine is cancelled the
            // snackbar will automatically dismiss. This coroutine will cancel whenever
            // `showSnackBar` is false, and only start when `showSnackBar` is true
            // (due to the above if-check), or if `scaffoldState.snackbarHostState` changes.
            val result = scaffoldState.snackbarHostState.showSnackbar(
                message = "Error message",
                actionLabel = "Retry message"
            )
            when (result) {
                SnackbarResult.Dismissed -> {
                    setShowSnackBar(false)
                }
                SnackbarResult.ActionPerformed -> {
                    setShowSnackBar(false)
                    // perform action here
                }
            }
        }
    }
    Scaffold(
        modifier = Modifier,
        scaffoldState = scaffoldState // attaching `scaffoldState` to the `Scaffold`
    ) {
        Button(
            onClick = {
                setShowSnackBar(true)
            }
        ) {
            Text(text = "A button that shows a Snackbar")
        }
    }
}

Upvotes: 7

Mahdi Zareei
Mahdi Zareei

Reputation: 2028

You can use this

@Composable
fun MainScreen() {
    val coroutineScope = rememberCoroutineScope()
    val showSnackBar: (
        message: String?,
        actionLabel: String,
        actionPerformed: () -> Unit,
        dismissed: () -> Unit
    ) -> Unit = { message, actionLabel, actionPerformed, dismissed ->
        coroutineScope.launch {
            val snackBarResult = scaffoldState.snackbarHostState.showSnackbar(
                message = message.toString(),
                actionLabel = actionLabel
            )
            when (snackBarResult) {
                SnackbarResult.ActionPerformed -> actionPerformed.invoke()
                SnackbarResult.Dismissed -> dismissed.invoke()
            }
        }
    }


    showSnackBar.invoke(
        "YOUR_MESSAGE",
        "ACTION_LABEL",
        {
         //TODO ON ACTION PERFORMED
        },
        {
         //TODO ON DISMISSED
        }
    )
}

Upvotes: 2

Bartek Lipinski
Bartek Lipinski

Reputation: 31438

You'll need two things:

  1. SnackbarHostState - which will manage the state of your Snackbar (you would usually get that from the ScaffoldState)
  2. CoroutineScope - which will be responsible for showing your Snackbar

@Composable
fun SnackbarDemo() {
  val scaffoldState = rememberScaffoldState() // this contains the `SnackbarHostState`
  val coroutineScope = rememberCoroutineScope()

  Scaffold(
        modifier = Modifier,
        scaffoldState = scaffoldState // attaching `scaffoldState` to the `Scaffold`
    ) {
        Button(
            onClick = {
                coroutineScope.launch { // using the `coroutineScope` to `launch` showing the snackbar
                    // taking the `snackbarHostState` from the attached `scaffoldState`
                    val snackbarResult = scaffoldState.snackbarHostState.showSnackbar(
                        message = "This is your message",
                        actionLabel = "Do something."
                    )
                    when (snackbarResult) {
                        Dismissed -> Log.d("SnackbarDemo", "Dismissed")
                        ActionPerformed -> Log.d("SnackbarDemo", "Snackbar's button clicked")
                    }
                }
            }
        ) {
            Text(text = "A button that shows a Snackbar")
        }
    }
}

example

Upvotes: 46

Related Questions