https://kotlinlang.org logo
#compose
Title
# compose
a

allan.conda

12/16/2020, 12:34 PM
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

nrobi

12/16/2020, 1:01 PM
IMO you could include a unique identifier in your
Error
a

allan.conda

12/16/2020, 1:15 PM
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

Timo Drick

12/16/2020, 1:16 PM
As long as you have access to scafoldState you can trigger the snackbar whenever you want.
a

allan.conda

12/16/2020, 1:17 PM
I can trigger it whenever, but it’s not triggering when want it to for some cases😄
t

Timo Drick

12/16/2020, 1:18 PM
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

allan.conda

12/16/2020, 1:35 PM
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

Timo Drick

12/16/2020, 1:38 PM
I will try to hack a minimal sample into a open repo so you can see how it is working.
a

Adam Powell

12/16/2020, 3:45 PM
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

allan.conda

12/16/2020, 4:00 PM
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

Adam Powell

12/16/2020, 4:10 PM
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

allan.conda

12/16/2020, 4:30 PM
Tried to write a minimal sample. I hope this helps illustrate the problem and what I expect
c

Colton Idle

12/16/2020, 4:38 PM
@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

Adam Powell

12/16/2020, 5:45 PM
Busy for a little while longer here but I'll come back to this later
t

Timo Drick

12/17/2020, 12:56 AM
@Adam Powell but how can i than cancel the coroutine? LaunchedEffect do not return a reference to the job
a

Adam Powell

12/17/2020, 2:37 AM
@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

Timo Drick

12/17/2020, 2:42 AM
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

Adam Powell

12/17/2020, 2:48 AM
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

Timo Drick

12/17/2020, 2:50 AM
Ok yes. That is a good point. I think i understand the problem now.
Thank you very much for your explanations Adam
👍 1
a

Adam Powell

12/17/2020, 2:55 AM
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

allan.conda

12/17/2020, 3:59 AM
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

Adam Powell

12/17/2020, 4:53 AM
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

allan.conda

12/17/2020, 9:06 AM
Thanks Adam! This is really great info.
👍 1
52 Views