Let's say you have some logic in your ViewModel th...
# compose
f
Let's say you have some logic in your ViewModel that should trigger a snackbar in your UI/Composable. How do you send that event? In XML times I used an event channel but this doesn't seem to work well with Compose since 2 times the same snackbar text in a row gets ignored by the
mutableStateOf
holder.
c
I've had this question and conversation way more times on this channel than I care to admit. And probably like 2-4 times in the past ~3 week. If you want to look through adam powells history on slack you can probably find some good conversations that way, but I also asked for more documentation and guidance surrounding this here if you want to star. https://issuetracker.google.com/issues/194911952
f
Thanks. Yea I read one of those threads but it was quite messy to read through
☝️ 1
c
Yeah. Messy and informative, but to the pragmatic person of "Hi. I'm a long time developer and now I am using compose. How do I do this?" it's not really reasonable. After all of those conversations I still don't really have a good answer. I suppose I have three approaches: 1. I try to make things state when I can, and following off of that, I just have been acknowleding that a snackbar was shown for example (just by resetting the state) 2. I use state to launch a LaunchedEffect or DisposableEffect (@zhuinden showed me some stuff around that here https://github.com/ColtonIdle/ComposeSignInSample/pull/1) 3. For navigation events, I basically call a viewmodel.doNetworkCall(success = mySuccessEvent, failure = myFailureEvent)
a
I’ve come across this article: https://codingtroops.com/android/compose-architecture-part-1-mvvm-or-mvi-architecture-with-flow/?s=09 haven’t tried but definitely going to :)
f
thanks, I'll look through that stuff in a bit!
nevermind that was for something else
@Colton Idle what problems did you run into using a normal Channel or SharedFlow for events?
c
Nothing. Except for the fact that it seemed like I needed a phd to understand anything related to flows and channels and such. I just need something that works. Emitting state, and then acking that a snackbar was shown was much easier, less error prone, and made sense to my teammates too. /shrug
f
and how do you reset that state so the snackbar is not shown again on screen rotation?
c
I just have something like If (state.showSnackBar){ showIt() State.showSnackBar = false } It's... Primitive. Maybe not the best. But it works.
f
yea it's an option
but resetting it manually sucks
m
I'm using Channel to show snackbar, refresh indicators, and such just like from the article Alexey shared (I'm actually following the exact pattern from the article) and I face no issue in that. What problem do you face by using Channel?
f
I also used to use channels in XML times
the problem in Compose is that the same value sent multiple times in a row gets swallowed by mutableStateOf
for example 2 times the same snackbar error message
m
ok, for this specific case, I'm not transforming the
Channel
into
mutableStateOf
. Like from the article, I expose the Channel as
Flow
to the composable. And in the composable, I use
LaunchedEffect(Unit)
to collect the flow and respond to that. This way I can consume all events emitted to the channel since the flow actually suspends until my task from
onEach/collect
is completed before emitting next item. Roughly like this,
Copy code
fun MyComposable(effectFlow: Flow<Effects>){
  LaunchedEffect(Unit){
    effectFlow.onEach{
        //do thing with it
    }.launchIn(this)
  }
}
c
At that point, couldn't you just use state and based on the state you just have a LaunchedEffect?
m
In this way, I can queue the event handling. If a snackbar message is emitted while another one is showing, the new one will be handled only after the previous task is finished since the flow operators are suspend functions.
f
@muthuraj but what do you do with the snackbar text you receive through the channel? You have to get it into the snackbar somehow.
But doing that with a State variable doesn't work
Copy code
var snackbarMessage by remember { mutableStateOf<String?>(null) } 

    LaunchedEffect(true) {
        viewModel.events.collect { event ->
            when (event) {
                is TodoListViewModel.Event.ShowTaskSavedConfirmationMessage ->
                    snackbarMessage = event.msg
            }
        }
this doesn't work properly but I don't see how else you can recompose to show the snackbar
m
In the collect block, I show the snackbar directly using
scaffoldState.snackbarHostState.showSnackbar
.
This way, if the next snackbar message comes in, it will wait until current snackbar is dismissed since the
showSnackbar
is a suspend function.
f
yea I guess that makes more sense
but for this the event collection and the snackbar code has to be in the same function right?
m
yeah
f
now that I look at it again, the snackbarMessage MutableState doesn't make sense anyway
because it remembers the value
exactly what we don't want
so I guess you declare the
scaffoldState
in the outer function and pass it down to the Composable that actually has the
Scaffold
? Is that correct?
m
My case is actually simpler. I use the
Scafflold
in the same function that I handle these events. But passing
scaffoldState
or even just
snackbarHostState
should work I think.
f
thank you
ok cool, then it's pretty similar to XML times
@Colton Idle I think you should give this approach a try
a Kotlin channel (not a SharedFlow) is imo the easiest way to get a single event to a single observer
it was also recommended in a blog post by the Kotlin developers
@muthuraj I actually don't like that the snackbars queue up. Do you know if there is a way to cancel the currently visible snackbar?
I can't find any method for that
i think clicking the same wrong button 5 times in a row should send 5 full-duration snackbars one after another
instead, the previous one should be canceled
m
You can use
collectLatest
instead of
collect.
*Latest
flow operators would cancel previous task when new event is received.
f
@muthuraj you're a genius
thank you
works perfectly
👍 2
c
@Florian looking forward to the video/blog post. Make this easy for me please. 🙏
😃 1
1
s
So the TL;DR is something like this?
Copy code
sealed class Effect {
    // different effects ...
}

class ViewModel {
    private val _effect = Channel<Effect>()
    val effect: Flow<Effect> = _effect.receiveAsFlow()
}

@Composable
fun Screen(viewModel: ViewModel) {
    LaunchedEffect(Unit) {
        viewModel.effect.collect { effect ->
            when (effect) {
                // handle cases ...
            }
        }
    }
}
f
Right. This is also what I did in XML times (minus the LaunchedEffect of course)
👍 1