https://kotlinlang.org logo
#android-architecture
Title
# android-architecture
o

OG

06/30/2020, 1:37 AM
Hello everyone, question about the MVI /MVVM architecture, I was wondering how folks handle navigation events / one shot events like showing a snack bar, navigating to a different fragment/activity, showing a dialog etc in their MVI/MVVM architecture. I know there's a SingleLiveEvent class created in a Google example repo, I've seen the Event wrapper solutions. But all of these are sort of workarounds. - The SingleLiveEvent option ties you to the MutableLiveData API and if that changes, your implementation breaks. Unless you create your own version that favors composition I suppose. -Using the Event wrapper to check if the value has been handled previously works, but brings about a redundant object wrapper and still doesn't fit with MVI/MVVM pattern of emitting a single state from your VM or w/e Is the only realistic approach to handling these one shot events as a separate thing from the "view state" that gets emitted in both MVI/MVVM patterns? Whats everyones thoughts?
g

gildor

06/30/2020, 1:42 AM
I think consumable eventwrapper is the only good solution for mutable ViewModel
SingleLiveEvent is not good just because it doesn’t support multiple subscribers and may cause tricky bugs related on this. But it can be improved by using PublishSubject + LiveData adapter, at least it supports proper multiple subscribers
still doesn’t fit with MVI/MVVM pattern of emitting a single state from your VM or w/e
Because navigation is event, it’s not state, there is no real solution, even with completely immutable ViewModel/State (like in unidirectional flow) you still have events to update state, so you have to emit some event in MVI it’s just some event (aka intent) to reducer to mutate state
m

mon

06/30/2020, 3:27 AM
For one-time events, you can represent them as a variant of a sum type but instead of exposing their data in `val`s, put them in a thunk instead. You extract the data by forcing the thunk, and as a side effect a new state is emitted which is the actual state you wanted to be in. https://kotlinlang.slack.com/files/UT7PZ7V26/FU1R3PVE1/logging_state.kt
👍🏽 1
m

Miguel Coleto

06/30/2020, 6:24 AM
My viewmodels expose a live data with the view state and a ReceiveChannel for View Events like those you are talking. SingleLiveData is a hack and we already have good abstractions with coroutines, no need to reinvent anything
p

pedro

06/30/2020, 10:05 AM
I prefer having a different stream for one-off events such as what @Miguel Coleto suggested. Regarding navigation, I’ve never done this but some people actually have the state represent the whole application. So navigation is part of that state. For example, you start in the Home Activity, your state could be `Home(… some vals…). But this is part of something else. Like a list with potentially several states. Each screen happens to render one of these. So opening a new screen is pushing a new element into that list (and maybe you need to bring another Activity/Fragment/View to render it). Pressing back is popping one or more elements from the end of that list. Of course that the top-level structure doesn’t have to be a list, that’s just my naive initial idea.
Otherwise, I’ve done navigation as one separate stream of events and it works well. When you want to navigate to a new activity, the current activity doesn’t necessarily mutate state. Whenever you return to it, you still want the same state there. So I wouldn’t use fake state updates for that.
o

OG

06/30/2020, 3:03 PM
First off, thanks to everyone who took some time to reply and give your thoughts. I had a feeling the more common approach would've been having a separate channel/stream/LD or w/e for one off events, and having a separate LD for just the view state. In the context of MVI it's still not 100% clear to me how we can make this work as the workaround I've seen is the state object in MVI holds immutable fields, and then fields that are wrapped with Event like so: data class State(val name: String, val showToast: Event<Unit>) And this 100% looks horrific to me... The single event there doesn't describe state and doesn't belong here So I'm trying to understand at least in MVI, when your reducer is given your old state and an event ( in this case an event that doesn't change state and signals a one off event), the reducer is supposed to emit a new state. And this is where I'll see the new state have that showToast field being set, or, just returns the oldState not modified, but also publishes to the different stream/channel like you guys mentioned. And not sure if either of these is really "correct" approach.
p

pedro

06/30/2020, 3:07 PM
And this 100% looks horrific to me.
I agree, that’s why I don’t send events to the reducer 😉 There are different alternatives to address that. I’d argue there’s no perfect solution. But here’s how two libraries address that: https://badoo.github.io/MVICore/features/news/ https://github.com/babylonhealth/orbit-mvi/ (Those links mention them and have different approaches)
o

OG

06/30/2020, 3:14 PM
I'll check them out. Thanks @pedro Cheers! 🍻
👍 1
a

Arkadii Ivanov

06/30/2020, 6:25 PM
MVIKotling has Labels, which is a separate stream of one time events. https://arkivanov.github.io/MVIKotlin/store.html
👍 1
o

OG

06/30/2020, 11:14 PM
I'll check this out, thanks!
g

gildor

07/01/2020, 1:29 AM
data class with event of course very strange thing, but in MVVM you don’t expose it as data class, you have interface for view model with Observable<SomeEvent>
and if you main point that event shouldn’t be part of your state itself, I agree, separate source of events is better solution, but it doesn’t contradict mentioned SingleLiveEvent or Event wrapper
t

tschuchort

07/01/2020, 1:23 PM
Personally I use a custom RxJava QueueSubject that queues when no one is subscribed but there are many open source implementations of this.
👍 1
1
o

OG

07/02/2020, 3:04 AM
I dig it. Thanks again everyone for your insight.
v

Viktor Petrovski

07/05/2020, 5:46 AM
In my current project we’ve implemented two custom classes by ourselves, but they are pretty simple. One is
ConsumableData
. You can access the data by calling consumeData() and after that it is set to null and the data is consumed so you can’t access it again. Another approach I adjusted is the SIngleLiveEvent for multiple observers:
Copy code
class SingleLiveEvent<T> : MutableLiveData<T>() {

    private val pending = AtomicBoolean(false)
    private val observers = mutableSetOf<Observer<in T>>()

    private val internalObserver = Observer<T> { t ->
        if (pending.compareAndSet(true, false)) {
            observers.forEach {
                it.onChanged(t)
            }
        }
    }

    @MainThread
    override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
        observers.add(observer)
        super.observe(owner, internalObserver)
    }

    override fun removeObservers(owner: LifecycleOwner) {
        observers.clear()
        super.removeObservers(owner)
    }

    override fun removeObserver(observer: Observer<in T>) {
        observers.remove(observer)
        super.removeObserver(observer)
    }

    @MainThread
    override fun setValue(t: T) {
        pending.set(true)
        super.setValue(t)
    }
}
Hope it helps 🤞
o

OG

07/05/2020, 2:49 PM
Very nice alternative! Thanks for the info! 🙌
😎 1
39 Views