Flow and LiveData in MVVM architecture

Lukasz Burcon
4 min readApr 8, 2020

Recently Coroutines have become more and more popular in the Android world. One of their pros is great integration with Flows and Channels that allow us to improve the way we execute asynchronous tasks in our projects. How should we design the architecture to make the best out of it?

It’s been some time since Kotlin Flow has been announced. With Coroutines 1.3.0 the first stable version of Flow has been released, yet we have to be aware that there are still some experimental APIs left in the library that may be a subject to change in the future.

Flow or LiveData for data sources?

LiveData was introduced as part of Architecture Components and it has integrated well in many projects since that time. It’s helpful for getting rid of callbacks and allows us to observe Lifecycle and act accordingly, getting rid of potential memory leaks.

On the other hand, Flow is designed to work in lower levels of architecture. For example, Room can return us Flow instances of the data we request for. What’s more, the Flow is observing the database like LiveData would, so it’s always up to date:

@Query("SELECT * FROM users")
fun getAllUsersFlow(): Flow<List<User>>

But you can say “LiveData is also supported by Room” — yes, that’s true. Does it support changing threads though? What about backpressure? Well, it’s all available in Flow — let’s take a look at our Repository:

// Repository.ktfun getAllUsersFlow(): Flow<List<User>> =
dao.getAllUsersFlow()
// do some mapping
.flowOn(Dispatchers.Default)
.conflate()

As you can see, we can easily change the thread we work on using flowOn() and handle backpressure by calling conflate() on the Flow chain that skips values emitted by this Flow if the collector is slower than emitter.

ViewModel

Can we use Flow in ViewModels? Of course we can! But for keeping the subscription active during orientation changes, it’s recommended to use LiveData when propagating data to a View. We can transform Flow to LiveData in two ways:

val users: LiveData<List<User>> = repository.getAllUsersFlow().asLiveData()

Or using the liveData builder function

val users: LiveData<List<User>> = liveData {
// some additional work
repository.getAllUsersFlow()
}

And then observe it as if it was a normal LiveData.

Now we can use LiveData for any lifecycle subscriptions between the ViewModel and the View. To keep the responsibilities separated, Flow will take care of threading and data sources operations, and propagating the results to LiveData.

Flow and Channel

In our example scenario we will only receive data whenever there’s an update to the users table. What if we want to get users that have a specific substring in their name every time we click on a new “Filter users” button?

Flows are “cold streams” — they will emit data when there’s an active observer registered to it (eg. Activity/Fragment observing the Flow converted to LiveData in our case). Whenever there’s a new observer added to Flow, the Flow will start a new execution for it. It’s hard to notify Flow that we need new values on a specific user action.

That’s where Channels come in handy. Channels, on the other hand, are “hot streams”, meaning they will emit data even if there’s no-one observing it. It’s both good and bad for us. The channel will keep emitting values even if there are no observers, thereby keeping all the dependencies it uses, which may lead to doing unnecessary work as it’s up to us if we close the channel. In our example scenario we don’t manually close the channel, as we want to continue listening for new values all the time the ViewModel stays alive.

Let’s modify our code to include a ConflatedBroadcastChannel:

// ViewModel.ktval usersChannel = ConflatedBroadcastChannel<String?>()val users: LiveData<List<User>> = usersChannel.asFlow()
.flatMapLatest { str ->
if (str.isNullOrBlank()) {
repository.getAllUsersFlow()
} else {
repository.getAllUsersWithCharacterFlow(str)
}
}.asLiveData()

We create a new ConflatedBroadcastChannel of type String, as we want to use the character value to be passed to filter our users. Next, assign the channel to our users and convert to Flow so we can have additional methods available. We then flatMapLatest, that helps with backpressure by restarting the whole block whenever a new value is received even if the previous computation has not finished yet. In this block we return an appropriate Flow from the Repository based on the string that’s passed in the usersChannel.

After doing the work in the block, we parse our flow to a LiveData, so we can still continue observing our users without any changes in Activities and Fragments!

How do we notify the Channel that new data is coming? Let’s implement an onClick for our button:

filterUsersButton.setOnClickListener {
viewModel.channel.offer(filterEditText.text.toString())
}

What’s the difference between offer() and send() in the Channel? The send() is a suspend function and should be called from a coroutine scope.

Conclusion

LiveData and Flow can coexist in one project. What’s more— they complement each other. While LiveData gives us reliability when configuration changes and opportunity to propagate up-to-date data to View, Flow works close with Channels in UseCase, Repository and DataSources layers to gather (and process) the data, executing tasks in different Coroutine scopes.

With that in mind, you can use LiveData just for ViewModel and View communication, leaving Flow to take care of more complex jobs such as threading in deeper layers.

--

--