Reputation: 417
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
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
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
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
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
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
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
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
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
Reputation: 31438
You'll need two things:
SnackbarHostState
- which will manage the state of your Snackbar
(you would usually get that from the ScaffoldState
)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")
}
}
}
Upvotes: 46