Blog Infos
Author
Published
Topics
, , , , ,
Author
Published
Posted by: Patryk  Kosieradzki

See the original article on my website:
https://patrykkosieradzki.com/livedata-vs-sharedflow-and-stateflow-in-mvvm-and-mvi-architecture/

If you’re a Polish speaker then you can listen to a podcast I created, based on this article:

Last year kotlinx.coroutines library introduced two new Flow types, SharedFlow and StateFlow, which also have their mutable types — MutableSharedFlow and MutableStateFlow.

Android community started wondering… Which one should I use now? LiveData or the new types? Is LiveData deprecated now?

Let’s answer all of the questions.

LiveData

Most of you should already know LiveData and how it works. LiveData is a data holder class that can be observed within a given lifecycle.

Example:
You create a LiveData object in a ViewModel class to hold some ViewStateand observe it in the Fragment to update UI when ViewState changes.

LiveEvent

OK, how about some single operations. For example, how can the ViewModel tell Fragment to show a Snackbar?

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 ModelViewIntent 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?

Unfortunately no.

  1. Flow is stateless, it has no .value property. It is just a stream of data that can be collected.
  2. 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.
  3. 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 countproperty 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

Job Offers

Job Offers


    Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Senior Android Software Engineer (f/m/d)

    Paradox Cat GmbH
    Munich
    • Full Time
    apply now

    Mobile Engineer

    OLX Group
    Remote, Portugal, Spain, Romania, Poland
    • Full Time
    apply now
Load more listings

OUR VIDEO RECOMMENDATION

Jobs

For example:

class FlowObserver<T>(
lifecycleOwner: LifecycleOwner,
private val flow: Flow<T>,
private val collector: suspend (T) -> Unit
) {
private var job: Job? = null
init {
lifecycleOwner.lifecycle.addObserver(
LifecycleEventObserver { source: LifecycleOwner, event: Lifecycle.Event ->
when (event) {
Lifecycle.Event.ON_START -> {
job = source.lifecycleScope.launch {
flow.collect { collector(it) }
}
}
Lifecycle.Event.ON_STOP -> {
job?.cancel()
job = null
}
else -> {
}
}
}
)
}
}
@InternalCoroutinesApi
inline fun <reified T> Flow<T>.observeOnLifecycle(
lifecycleOwner: LifecycleOwner,
noinline collector: suspend (T) -> Unit
) = FlowObserver(lifecycleOwner, this, collector)
@InternalCoroutinesApi
fun <T> Flow<T>.observeInLifecycle(
lifecycleOwner: LifecycleOwner
) = FlowObserver(lifecycleOwner, this, {})
view raw FlowObserver.kt hosted with ❤ by GitHub

Then we can use it like this:

// ViewModel with some Flows to observe and collect
abstract class BaseViewModel<STATE, EVENT : UiEvent, EFFECT : UiEffect>(
initialState: UiState<STATE> = UiState.Loading
) : ViewModel() {
...
private val _uiState: MutableStateFlow<UiState<STATE>> = MutableStateFlow(initialState)
val uiState = _uiState.asStateFlow()
private val _event: MutableSharedFlow<EVENT> = MutableSharedFlow()
val event = _event.asSharedFlow()
private val _effect: Channel<EFFECT> = Channel()
val effect = _effect.receiveAsFlow()
abstract fun handleEvent(event: EVENT)
...
}
// And how to collect them in the Fragment
abstract class BaseFragment<STATE, EVENT : UiEvent, EFFECT : UiEffect,
VM : BaseViewModel<STATE, EVENT, EFFECT>, VDB : ViewDataBinding>(
@LayoutRes private val layoutId: Int,
vmKClass: KClass<VM>
) : Fragment() {
...
@InternalCoroutinesApi
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(viewModel) {
event
.onEach { handleEvent(it) }
.observeInLifecycle(viewLifecycleOwner)
uiState
.onEach { handleState(it) }
.observeInLifecycle(viewLifecycleOwner)
}
}
...
}
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?

  1. SharedFlow in contrast to a normal Flow is hot, every collector uses the same SharedFlow, because it is shared.
  2. 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.
  3. If you use the default SharedFlow constructor of MutableSharedFlow then the replay cache won’t be created.

You can also transform a normal Flow into a SharedFlow using this extension method:

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:

// ViewModel with SharedFlow
abstract class BaseViewModel<STATE, EVENT : UiEvent, EFFECT : UiEffect>(
initialState: UiState<STATE> = UiState.Loading
) : ViewModel() {
...
private val _event: MutableSharedFlow<EVENT> = MutableSharedFlow()
val event = _event.asSharedFlow()
abstract fun handleEvent(event: EVENT)
...
}
// And how to pass info to ViewModel to handle the event
abstract class BaseFragment<STATE, EVENT : UiEvent, EFFECT : UiEffect,
VM : BaseViewModel<STATE, EVENT, EFFECT>, VDB : ViewDataBinding>(
@LayoutRes private val layoutId: Int,
vmKClass: KClass<VM>
) : Fragment() {
...
@InternalCoroutinesApi
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
with(viewModel) {
event
.onEach { handleEvent(it) }
.observeInLifecycle(viewLifecycleOwner)
}
}
...
}

And then in ViewModel you can handle triggered events:

class AddEmployeeViewModel(
private val employeeRepository: EmployeeRepository
) :
BaseViewModel<AddEmployeeContract.State, AddEmployeeContract.Event, AddEmployeeContract.Effect>(
initialState = AddEmployeeContract.State.Loading()
) {
...
override fun handleEvent(event: AddEmployeeContract.Event) {
when (event) {
is AddEmployeeContract.Event.AddAddressEvent -> {
updateForm(
address = "",
addresses = currentState.addresses.plus(Address(currentState.address))
)
}
is AddEmployeeContract.Event.RemoveAddressEvent -> {
val addresses = currentState.addresses.toMutableList()
addresses.remove(event.address)
updateForm(addresses = addresses)
}
}
}
fun onAddAddressClicked() {
setUiEvent(AddEmployeeContract.Event.AddAddressEvent)
}
fun onRemoveAddressClicked(address: Address) {
setUiEvent(AddEmployeeContract.Event.RemoveAddressEvent(address))
}
}

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?

StateFlow is a SharedFlow with a couple other things:

  1. When creating a StateFlow you have to provide its initialState.
  2. You can access StateFlow’s current state by .value property, just like in LiveData.
  3. 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.

You can also transform a normal Flow into a StateFlow using this extension method:

fun <T> Flow<T>.stateIn(     
  scope: CoroutineScope,      
  started: SharingStarted,      
  initialValue: T 
): StateFlow<T> (source)

Let’s see how we can use StateFlow and SharedFlow together to update state after event is handled. The BaseViewModel and BaseFragment would look something like this:

And then in ViewModel you can update state like this:

class AddEmployeeViewModel(
private val employeeRepository: EmployeeRepository
) :
BaseViewModel<AddEmployeeContract.State, AddEmployeeContract.Event, AddEmployeeContract.Effect>(
initialState = AddEmployeeContract.State.Loading()
) {
...
override fun handleEvent(event: AddEmployeeContract.Event) {
when (event) {
is AddEmployeeContract.Event.AddAddressEvent -> {
updateForm(
address = "",
addresses = currentState.addresses.plus(Address(currentState.address))
)
}
is AddEmployeeContract.Event.RemoveAddressEvent -> {
val addresses = currentState.addresses.toMutableList()
addresses.remove(event.address)
updateForm(addresses = addresses)
}
}
}
fun updateForm(
firstName: String? = null,
lastName: String? = null,
address: String? = null,
addresses: List<Address>? = null,
) {
updateUiState {
AddEmployeeContract.State.FormUpdated(
firstName = firstName ?: currentState.firstName,
lastName = lastName ?: currentState.lastName,
address = address ?: currentState.address,
addresses = addresses ?: currentState.addresses
)
}
}
fun onAddAddressClicked() {
setUiEvent(AddEmployeeContract.Event.AddAddressEvent)
}
fun onRemoveAddressClicked(address: Address) {
setUiEvent(AddEmployeeContract.Event.RemoveAddressEvent(address))
}
}

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! 🙂


Tags: Mvi, Mvvm, Kotlin, Android, Android Architecture

 

View original article at:


Originally published: July 14, 2021

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Three years ago I wrote an article about binding a list of items to…
READ MORE
blog
As Kotlin’s Flow library has stabilized and matured, Android developers have been encouraged to…
READ MORE
blog
It’s hard to imagine building an Android application in 2022 and not reaching for…
READ MORE
blog
As an Android developer, I am sure you have come across the term MVVM…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu