If you’re a Polish speaker then you can listen to a podcast I created, based on this article:
Let’s answer all of the questions.
By using the LiveEvent (or SingleLiveEvent), a modified LiveData to handle single events, which means it emits the data just once, not after configuration changes again. The same solution can be used to display a toast, dialog, navigate to other Fragment, etc.
So what is exactly the problem with LiveData and LiveEvents?
LiveData is an Android library
As you know, LiveData is a part of Jetpack and it is an Android library. It is has to be handled in Android classes and with their lifecycle. It is closely bound to the UI, so there is no natural way to offload some work to worker threads.
In Clean Architecture terms, LiveData is OK to be used in Presentation Layer, but it is unsuitable for other layers, like Domain for example, which should be platform-independent (only Java/Kotlin module), Network Layer, Data Layer, etc.
LiveData is OK for MVVM, but not so much for MVI
MVI stands for Model–View–Intent and it’s a design pattern that uses Unidirectional Data Flow to achieve something like we already have in Flux or Redux, etc.
As you can see, the picture above shows the desired Data Flow that should be used in MVI. View communicates with the ViewModel by triggering events which are then handled inside the ViewModel’s logic, UseCases, etc. At the end, the new ViewState is emitted and UI is updated.
Handling view states using LiveData is pretty easy and can be used both for MVVM and MVI, but the problem begins when we want to show a simple Snackbar like before. If we use the LiveEvent class for that then the whole Unidirectional State Flow is disturbed, since we just triggered an event in ViewModel to interact with UI, but it should be the opposite way.
So, now we’ve just created a combination of MVVM and MVI and we confuse a lot of people on what is an event exactly and how the architecture works. Not cool, right?
To get it right in MVI you should treat these single “events” as side effects. You can use Channels for that, but this is a topic for another article.
BTW. Don’t worry, LiveData is not going to be deprecated. You can still use it if you like it 🙂
OK, so what now? We have SharedFlow, StateFlow, but we had Flow already in Kotlin before. Can’t we use it?
- Flow is stateless, it has no .value property. It is just a stream of data that can be collected.
- Flow is declarative (cold). It is only materialized when collected and for each new collector there will be a new Flow created. This is not a good option for doing expensive stuff, like accessing the database and other things that don’t have to be repeated every time.
- Flow has no idea about Android and lifecycles. It doesn’t provide automatic starting, pausing, resuming of collectors upon Android lifecycle state changes.
BUT WAIT, (3) is not so true now…
This was solved by adding an extension method launchWhenStarted to LifecycleCoroutineScope, but I see most people don’t know how to use it properly. It is simply not enough, since Flow has a subscription count property that won’t be changed when Lifecycle.Event reaches ON_STOP. This means that the Flow will be still active in memory and could cause memory leaks!
To solve this problem you can create a custom observer that will launch collect method when ON_START event is triggered and cancel its job on ON_STOP. There are also other methods that you can use to achieve the same result, like repeatOnLifecycle or flowWithLifecycle.
Then we can use it like this:
SharedFlow and StateFlow to the rescue!
Let’s talk about SharedFlow first
SharedFlow is a type of Flow that shares itself between multiple collectors, so it is only materialized once for every subscriber. What else it can do?
- SharedFlow in contrast to a normal Flow is hot, every collector uses the same SharedFlow, because it is shared.
- SharedFlow has its buffer called replay cache. It keeps a specific number of the most recent values in it. Every new subscriber gets the values from the replay cache and then gets new emitted values. You can set the maximum size of the replay cache in replay parameter in the constructor. A replay cache also provides buffer for emissions to the shared flow, allowing slow subscribers to get values from the buffer without suspending emitters. A SharedFlow with a buffer can be configured to avoid suspension of emitters on buffer overflow using the onBufferOverflow parameter, which is equal to one of the entries of the BufferOverflow enum. When a strategy other than SUSPENDED is configured, emissions to the shared flow never suspend.
- If you use the default SharedFlow constructor of MutableSharedFlow then the replay cache won’t be created.
fun <T> Flow<T>.shareIn( scope: CoroutineScope, started: SharingStarted, replay: Int = 0 ): SharedFlow<T> (source)
Let’s see how we can use SharedFlow to handle events. The BaseViewModel and BaseFragment would look something like this:
And then in ViewModel you can handle triggered events:
onAddAddressClicked() and onRemoveAddressClicked(address: Address) are used in DataBinding, so we have to trigger events here. Can you see how simple this is? Clean, readable code that can be easily tested (tests included in the Github Repository at the end of the article)
What about StateFlow?
- When creating a StateFlow you have to provide its initialState.
- You can access StateFlow’s current state by .value property, just like in LiveData.
- If you add a new collector in the meantime then it will automatically receive current state. Also, it won’t get any info about previous states, but only the new ones that will be emitted.
fun <T> Flow<T>.stateIn( scope: CoroutineScope, started: SharingStarted, initialValue: T ): StateFlow<T> (source)
And then in ViewModel you can update state like this:
That’s it! Simple, right?
Example Github Repository (Jetpack Compose, MVI)
I’ve created a Github Repository with examples on standard Fragments and Jetpack Compose with SharedFlow and StateFlow. There are also unit tests and UI/Screenshot tests included for you to see how easy it is to test MVI.
You can find it here: https://github.com/k0siara/AndroidMVIExample
Write a comment if you want to ask anything and I wish you happy coding! 🙂