What’s the best practice for triggering snackbars ...
# compose
a
What’s the best practice for triggering snackbars (outside onClick)?
Copy code
LaunchedEffect(error) {
  snackbarState.showSnackbar(error.message)
}
I have this but this doesn’t run if exactly the same error happens.
n
IMO you could include a unique identifier in your
Error
a
Thanks. The sample is too simple, it just shows a snackbar immediately on click.
I need to show one after a network failure for example
@nrobi Thanks, that’s a good one, if not the best. Can be restored after process death as well without retriggering the snackbar. Weirdly I’ve never seen that approach here before though.
t
As long as you have access to scafoldState you can trigger the snackbar whenever you want.
a
I can trigger it whenever, but it’s not triggering when want it to for some cases😄
t
Before this SnackbarHost existed i implemented (and still using) my own custom implementation:
Copy code
class SnackbarController() {
    internal val snackbarList = mutableStateListOf<@Composable SnackbarScope.() -> Unit>()
    fun showSnackbar(snackbar: @Composable SnackbarScope.() -> Unit) {
        snackbarList.add(0, snackbar)
    }
    fun removeSnackbar(snackbar: @Composable SnackbarScope.() -> Unit) {
        snackbarList.remove(snackbar)
    }
}

@Composable
fun SnackbarContainer(modifier: Modifier = Modifier, snackbarController: SnackbarController) {
    val list = snackbarController.snackbarList
    list.firstOrNull()?.let { snackbar ->
        val scope = remember(snackbar) { SnackbarScope(snackbarController, snackbar) }
        Box(modifier) {
            snackbar(scope)
        }
    }
}
class SnackbarScope(private val snackbarController: SnackbarController, private val snackbar: @Composable SnackbarScope.() -> Unit) {
    fun dismiss() {
        snackbarController.removeSnackbar(snackbar)
    }
}
You need some more things to get it really running and also dismiss after a delay e.g.:
Copy code
@Composable
fun SnackbarUI(modifier: Modifier = Modifier, text: String, actionText: String, onAction: () -> Unit, onDismiss: () -> Unit) {
    val scope = rememberCoroutineScope()
    val job = scope.launch {
        delay(5000)
        onDismiss()
    }
    Snackbar(
            modifier = modifier,
            text = { Text(text, style = MaterialTheme.typography.button) },
            action = {
                FaTextButton(onClick = { job.cancel(); onAction() }) { Text(actionText) }
            }
    )
}
And usage is than:
Copy code
snackbarController.showSnackbar {
                        SnackbarUI(
                                text = "Removed ${item.imageData.displayName ?: ""}\n${item.imageData.category.title}",
                                actionText = "Undo",
                                onAction = { dismiss() },
                                onDismiss = { dismiss() }
                        )
                    }
a
I have this but this doesn’t run if exactly the same error happens.
^ this is the use case I need to handle
the async operation is outside the composable
t
I will try to hack a minimal sample into a open repo so you can see how it is working.
a
don't do this in a
@Composable
function:
Copy code
val job = scope.launch {
we should probably have a lint error for any use of
CoroutineScope.launch
or
CoroutineScope.async
inside of a composable function. You want
LaunchedEffect
instead.
☝️ 1
a
Hi Adam! 🙂 Any idea here? This issue just started to happen from alpha08 (at least) actually, it seems new optimization code got in and now checks for equality to avoid recomposition.
a
For the original question? Yes, that's what I would expect 🙂 can you show the code where the error originates and how it gets to the snippet in the OP?
👍 1
a
Tried to write a minimal sample. I hope this helps illustrate the problem and what I expect
c
@Adam Powell yessss. need more lint warnings/errors. I feel like it's going to be critical to get people going in the right direction during the early adoption phase.
a
Busy for a little while longer here but I'll come back to this later
t
@Adam Powell but how can i than cancel the coroutine? LaunchedEffect do not return a reference to the job
a
@Timo Drick it doesn't have to, the job is maintained by the composition. If one of the other parameters changes, the old job will be cancelled and a new one launched. If the
LaunchedEffect
leaves the composition entirely, the job will be cancelled.
whenever there's a confusion between state and events, this thing is never far behind...
Copy code
data class Event<out T>(val content: T) {
    var consumed: Boolean = false
t
Yes i know. But in my usecase i want to cancel the job before the compose is changed because otherwise the onDismiss() is called.
a
you've got a bigger problem of identity though; remember, that function can be recomposed at any time for any reason, and you're launching that delay+dismiss every time
it's overwriting your click handler with one pointing at the new job each time, so the old jobs are still running in the remembered scope, and you only have the means to cancel the most recent one.
t
Ok yes. That is a good point. I think i understand the problem now.
Thank you very much for your explanations Adam
👍 1
a
Compose likes to work in terms of stable state. Error displays come in different flavors depending on what originated them. The idea of a "current" error is state; if you're displaying an error with a timeout, launch the timeout with a subject/key parameter of the current error. If you press a button to acknowledge the error, clear the current error - it will naturally cancel the timeout.
👍 1
this is why it's significant that different pieces of state have identity; when you show the same or equal data, compose considers it the same no matter how many times that particular composable recomposes
using constructs like those consumable
Event
wrappers is kind of like trying to ice skate uphill against the mental model; it's a way to still try to think in terms of each recomposition or run as its own significant event, but model it as state just enough to make things sort of look like they work
Event
wrappers try to circumvent compose's pull towards idempotence
a
That makes sense! I think I see what you mean. I think this should work, or is there a better way to do this?
a
Looks good to me 👍
Skip the event wrappers, treat it as part of your state, clear the error after the user or some other part of your system acknowledges it
👍 2
a
Thanks Adam! This is really great info.
👍 1
173 Views