https://kotlinlang.org logo
Title
a

André Tessmer

05/09/2023, 7:11 PM
hello everyone. how are you handling one-time events between the viewmodel and compose? snackbars and navigation for example
m

Marcin Wisniowski

05/09/2023, 7:40 PM
You can model the events as part of your state. Your ui state can have a
message
String, and you can show a Snackbar when it's set, while also informing your ViewModel that you did, so it can set it back to null. Or some other mechanism that makes it only readable once.
c

Colton Idle

05/09/2023, 7:46 PM
Yep! Everything Marcin talked about is shown in detail here: https://developer.android.com/topic/architecture/ui-layer/events
a

André Tessmer

05/09/2023, 8:01 PM
I don't really like the idea of having to call the ViewModel to say that the event has been handled, doesn't seem scalable. what do you guys think about wrapping the event in a class? something like:
open class Event<out T>(private val content: T) {

    var consumed = false
        private set

    
    fun consume(): T? {
        return if (consumed) {
            null
        } else {
            consumed = true
            content
        }
    }
}
also, I am trying to work with separated flows for UiState and NavigationState, what do you think about that? what are the concerns? any pros and cons?
private val _uiState = MutableStateFlow(SelectWallpaperUiState.initial)
    val uiState = _uiState.asStateFlow()

    private val _navigationState = MutableStateFlow<SelectWallpaperNavigationState>(SelectWallpaperNavigationState.None)
    val navigationState = _navigationState.asStateFlow()
the first problem comes right here, since stateflow requires you to provide a initial value
and when creating the navigation StateFlow, we don't actually need/have a starting navigation event
m

Marcin Wisniowski

05/09/2023, 8:13 PM
Looks like your "navigationState" isn't a state then, and shouldn't use a StateFlow. You can use a SharedFlow if you don't want to have an initial state.
p

Prateek Kumar

05/09/2023, 8:13 PM
I way i do it to have a navigationEvent with an initial value of NoEvent , and every time i fire a navigationEvent it's UI responsibility to make it to NoEvent again, works both in compose and xml
And i use stateFlow only for this
Bcz if u use sharedFlow , it will work in XML but on configuration changes or if u use compose, it might create more problems then it will solve
d

dewildte

05/09/2023, 8:26 PM
I like the approach as per Google's recommendation. In my experience it does scale, opposite ones initial intuition. It also has a nice side effect of making the code far more understandable and debuggable. It comes only at one cost, effort. A worthy price to pay!
a

André Tessmer

05/09/2023, 8:28 PM
Bcz if u use sharedFlow , it will work in XML but on configuration changes or if u use compose, it might create more problems then it will solve
@Prateek Kumar do you mind explaining in more detail the possible problems?
so I think I'll have a
navigationEvent: Event<SelectWallpaperNavigationState>
inside my state
what do you guys think about that?
this way I keep the events inside my state, and it being an
Event
it will also handle the
one-shot
thing
p

Prateek Kumar

05/09/2023, 8:38 PM
I mean the benefit of sharedFlow will be not giving initial value and no difference check but it also requires some extra setup like replay 1 and befferoverflow drop oldest i think. Also, only for one-off event u will use shredFlow and for others u will use stateFlow? Or u can go all out sharedFlow only. But if u think of ur UI being replica of your state i don't see why stateFlow is not self sufficient. Have a initial NoState and once a state it handled set it back to NoState properly.
This is how i keep all my one-off events in just one place
d

dewildte

05/09/2023, 8:42 PM
I think the problem is really that the ViewModel even knows anything about navigation at all. The ViewModel is meant to be a state holder. The UI is a reflection of that state. Also navigation is purely a UI framework implementation detail. Those facts lead to all this confusion around how to handle navigation and events from a construct that is specifically designed to not care about that.
Putting the wrong type of logic into the ViewModel will have sideeffects on your architecture that is simply not pragmatic let alone scalable.
Let me give you an example. Lets say the user is filling out a form. When the form is completed and submitted with out issue then the user should be navigated back from where they came. The form state could look like so:
data class FormState(val isCompleted: Boolean = false)
When the UI controller (Composable, Fragment, or Activity) observes that
isCompleted
is
true
then it decides to perform the back navigation.
The
ViewModel
does not need to know about UI details like navigation. That’s also why you might see the examples given by Google where the UI shows a
SnackBar
if a a
String
on a state in a
ViewModel
!= null
and then hiding the
SnackBar
if it is
null
. The ViewModel layer knows nothing about a purely UI concern. This makes things easier to test, maintain, reason about, and safer under some conditions.
c

Colton Idle

05/09/2023, 9:45 PM
also. view the bottom of the docs i linked about event streams being an anti pattern.
2023-05-09 at 17.45.30.png
p

Pablichjenkov

05/09/2023, 10:16 PM
There was a recent discussion about the subject. And yes Marcin answer is the accepted way. There doesn't seem to be a guarantee of "one time delivery" successfully. Due to config changes or screen off. So yeah verbose and boilerplate but that's the latest trend. I still gotta migrate a lot of channel based communication to this 🤫
t

Tim Malseed

05/09/2023, 11:13 PM
p

Pablichjenkov

05/09/2023, 11:21 PM
Yeah, that article was the trigger in the last discussion
a

Albert Chang

05/10/2023, 4:52 AM
For snackbars, I'm using this pattern suggested by Adam and I really like it. Simple and easy to test.