Migrating from LiveData to Flow in 5 steps

A Guide from Our Experienced Android Developer

Migrating from LiveData to Flow in 5 steps

Introduction

This is my first post on Medium (updated on 19/04/2021 with the correct way to collect Flow inside Fragments), but I feel it might be useful for someone looking for a simple guide to move on from LiveData to Kotlin Flow (especially that now StateFlow can be used with data binding).

First, why would you want to move from LiveData?

There are quite a lot of small improvements that separately don’t seem like worth refactoring, but once you put them all together they definitely justify the change. The biggest benefits:

  1. More powerful transformations. Transform, filter and do anything with the data.
  2. Can be combined. Combine as much flows as you need.
  3. Collectable from any dispatcher. No more need to set Dispatchers.Main after fetching data on asynchronously on Dispatchers.IO.
  4. Adjustable (replays, buffers, etc.). More flexibility on how to handle data and errors.
  5. Convertible from cold to hot (stateIn). We can easily convert a cold flow to StateFlow using stateIn(). Please, watch a video by Android Developers attached in the end of this post as it explains it really great!
  6. SingleLiveEvent made easy. (because we don’t want our error dialogs pop-up on each rotation) Channel is a great solution that is convertible to a Flow with a simple extension function.
  7. Easier to test. No more getOrAwaitValue or observeForTesting.
  8. Better for multiplatform projects. No need to re-write code since they’re not tied to specific platform, unlike LiveData.

To learn more about the features of Flows and why it’s a good choice to move on from LiveData, please, look at this articles in the end of this post (especially the one by @JoseAlcerreca who has a very detailed article with in-depth explanations).

"Simple things are harder and complex things are easier"

@JoseAlcerreca

Also, you will see some examples in this article where you will be able to see these benefits yourself. However, for the main part, I’ll try to show how easy and painless the transition can be.

Before you start, make sure you use the following dependency with at least version 2.4.0:

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'

Let’s get started!

Step 1: Changing you BaseViewModel.

Since most probably you have SingleLiveEvent in it, let’s see how you can update it using Channel. Here is a simplified BaseViewModel:

abstract class BaseViewModelWithLiveData() : ViewModel() {
val navigationCommand: SingleLiveEvent = SingleLiveEvent()
val showErrorMessage: SingleLiveEvent = SingleLiveEvent()
val showLoading: MutableLiveData = MutableLiveData()
}

There will be a little bit more code for the version with Flow, since we will use Coroutines to update the value and expose the properties as Flow (and this is the only place where code gets somewhat bigger!):

abstract class BaseViewModelWithFlow() : ViewModel() {
private val _navigationCommand = Channel(capacity = CONFLATED)
val navigationCommand = _navigationCommand.receiveAsFlow()
fun navigate(navigationCommand: NavigationCommand) {
viewModelScope.launch {
_navigationCommand.send(navigationCommand)
}
}
private val _showErrorMessage = Channel(capacity = 1)
val showErrorMessage = _showErrorMessage.receiveAsFlow()
fun showErrorMessage(message: String) {
viewModelScope.launch {
_showErrorMessage.send(message)
}
}
val showLoading = MutableStateFlow(false)
}

Step 2: Changing your BaseFragment.

abstract class BaseFragmentWithLiveData : Fragment() {
abstract val viewModel: BaseViewModelWithLiveData
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.showErrorMessage.observe(viewLifecycleOwner) { message ->
// Show some error
}
viewModel.navigationCommand.observe(viewLifecycleOwner) { command ->
// Navigate to another destination
}
viewModel.showLoading.observe(viewLifecycleOwner) { isLoading ->
// Show loading
}
}
}

Before we continue, let’s have a look at the correct way to collect Flow inside the Fragments (if you will use repeatOnLifecycle(Lifecycle.State.Started) you will get “The repeatOnLifecycle API should be used with viewLifecycleOwner” warning) Here is the correct solution provided by Google: launchAndRepeatWithViewLifecycle

So our Fragment will change to:

abstract class BaseFragmentWithFlow : Fragment() {
abstract val viewModel: BaseViewModelWithFlow
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
launchAndRepeatWithViewLifecycle {
launch {
viewModel.showErrorMessage.collect { message ->
// Show some error
}
}
launch {
viewModel.navigationCommand.collect { command ->
// Navigate to another destination
}
}
launch {
viewModel.showLoading.collect { isLoading ->
// Show loading
}
}
}
}
}

Step 3: Changing your ViewModel.

Let’s use a very simple example, like this one:

class ViewModelWithLiveData() : BaseViewModelWithLiveData() {
private val _someList = MutableLiveData()
val someList: LiveData = _someList
}

To replace MutableLiveData we will use MutableStateFlow, and expose it as StateFlow for our UI using asStateFlow():

class ViewModelWithFlow() : BaseViewModelWithFlow() {
private val _someList = MutableStateFlow(listOf())
val someList = _someList.asStateFlow()
}

So far, we’ve looked at simple things that are harder — now let’s look at more complex things that get so much easier!

Let’s start with a simple ViewModel for some RegistrationFragment, where you want to check if the user filled in all the fields before enabling the “Register” button.

LiveData offers us MediatorLiveData to observe multiple LiveData objects, however, it adds a lot of repetitive code — the more input fields you have, the longer and more complex will the validation become:

class RegisterViewModelWithLiveData() : BaseViewModelWithLiveData() {
// Input fields to validate
val nickname = MutableLiveData("")
val name = MutableLiveData("")
val surname = MutableLiveData("")
val email = MutableLiveData("")
val password =MutableLiveData("")
private fun _canSave(): MutableLiveData {
val result = MediatorLiveData()
result.addSource(nickname) {
result.value = infoComplete()
}
result.addSource(name) {
result.value = infoComplete()
}
result.addSource(surname){
result.value = infoComplete()
}
result.addSource(email){
result.value = infoComplete()
}
result.addSource(password){
result.value = infoComplete()
}
return result
}
// LiveData for UI to observe
val canSave: LiveData = _canSave()
private fun infoComplete(): Boolean {
return nickname.value!!.isNotEmpty()
&& name.value!!.isNotEmpty()
&& surname.value!!.isNotEmpty()
&& email.value!!.isNotEmpty()
&& password.value!!.isNotEmpty()
}
fun register() {
// Regsiter the user
}
}

If you add more complex validation, the code will get even longer and hardly readable.

Using Flow you can just use combine() function and combine up to 5 Flow out-of-the-box or add an extension function to support more. And that’s it:

class RegisterViewModelWithFlow() : BaseViewModelWithFlow() {
// Input fields to validate
val nickname = MutableStateFlow("")
val name = MutableStateFlow("")
val surname = MutableStateFlow("")
val email = MutableStateFlow("")
val password = MutableStateFlow("")
// We can combine up to 5 Flow, but can expand the number with simple extension functions
val canSave: StateFlow =
combine(nickname, name, surname, email, password)
{ nickname, name, surname, email, password ->
nickname.isNotEmpty()
&& name.isNotEmpty()
&& surname.isNotEmpty()
&& email.isNotEmpty()
&& password.isNotEmpty()
}
.stateIn(
initialValue = false,
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000)
)
fun register() {
// Regsiter the user
}
}

Exactly the same validation but simpler and cleaner!

In case you have database queries, you most probably usually use Transformations.switchMap(). However, even after transformation we cannot directly expose the list to our UI as we still need to transform it to domain model. This adds more properties to our code and the more transformations we have, the longer our code becomes:

class SomeViewModelWithLiveData (
private val repo: SomeRepository
) : BaseViewModelWithLiveData() {
private val _query = MutableLiveData()
private val _someList: LiveData = Transformations.switchMap(_query){ query ->
when(query){
"" -> repo.getAllSomeItems()
else -> repo.findSomeItems(query)
}
}
val someList: LiveData = Transformations.map(_someList) { list ->
list.toDomainModel()
}
}

Here we have 3 variables and multiple transformations. Let’s look how we can achieve the same result with flow with only 1 variable (switchMap analog for Flow is flatMapLatest — still ExperimentalCoroutinesApi)

@ExperimentalCoroutinesApi
class SomeViewModelWithFlow (
private val repo: SomeRepository
) : BaseViewModelWithFlow() {
private val _query = MutableStateFlow("")
val someList: StateFlow = _query
.flatMapLatest { query ->
when (query) {
"" -> repo.getAllSomeItems()
else -> repo.findSomeItems(query)
}
}
.map { list ->
list.toDomainModel()
}
.stateIn(
initialValue = listOf(),
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000)
)
}

With Flow we have only one variable and we can add as many transformations as we need.

We can also have additional suspend functions while transforming our data without any boilerplate. Feeling excited? Because I definitely am.

This way of using suspend function during transformations can be used for an additional Api calls (if you save some items but don’t save details and want to fetch every time), sub-queries, etc.

Let’s take a very simple example, when you want to save space on the device and save only favorite SomeItems in the database (even though it’s better to save them all, but we will assume it’s really important to save even that small space!)

Let’s assume there are 3 types of SomeItem in our application:

  • SomeItem — parsed from JSON
  • DomainSomeItem domain
  • DBSomeItem — database

We need to check if the item parsed from JSON (SomeItem) was added to our favorite table (DBSomeItem). So when we are transforming SomeItem into DomainSomeItem to expose it to our UI, we need to find if we have the same item in our table of and show that it’s a favorite one.

class AdvancedViewModelWithFlow() : BaseViewModelWithFlow() {
private val _someList = MutableStateFlow(listOf())
val someList: StateFlow = _someList
.asStateFlow()
.map { list ->
list.map { someItem ->
someItem.toDomainModel().apply {
val dbSomeItems: Flow = someRepository.findSomeItem(this.name).first()
this.favorite = dbSomeItems.isNotEmpty()
this.id = dbSomeItems.firstOrNull()?.id
}
}
}
}

Ready! As simple as that. These are some specific cases which you might need or might not, but it’s definitely worth having these super-powers once you’ll start adding functionality to your application.

Step 4: Observe the data inside your Fragment.

This will be exactly the same as for our BaseFragment.

Step 5: Update your Unit tests!

Probably, your test look similar to the code below:

@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
class ViewModelWithLiveDataTest {
// Some test set-up here and add a fake repo
@Test
fun loadSomeDataReturnEmpty() {
repository.setReturnEmpty(true)
mainCoroutineRule.pauseDispatcher()
viewModel.loadSomeData() // Some function in our ViewModel to fetch SomeItems!
assertEquals(true, viewModel.showLoading.getOrWaitValue())
mainCoroutineRule.resumeDispatcher()
assertEquals(listOf(), viewModel.someList.getOrWaitValue())
assertEquals(false, viewModel.showLoading.getOrWaitValue())
}
}

And to update it for flow:

@ExperimentalCoroutinesApi
@RunWith(JUnit4::class)
class ViewModelWithFlowTest {
@get: Rule
var mainCoroutineRule = MainCoroutineRule()
// Some test set-up here and add a fake repo
@Test
fun loadSomeDataReturnEmpty() = mainCoroutineRule.runBlockingTest {
repository.setReturnEmpty(true)
val job: Job?
val showLoadingResult = mutableListOf()
var dataResult: List? = null
job = launch {
viewModel.showLoading.toList(showLoadingResult)
}
viewModel.loadSomeData() // Some function in our ViewModel to fetch SomeItems!
dataResult = viewModel.someList.first()
assertEquals(listOf(false, true, false), showLoadingResult)
assertEquals(dataResult, listOf())
job.cancel()
}
}

Not only you get rid of boilerplate code, pausing dispatcher, but also in cases with MediatorLiveData you will get rid of observeForTesting. There are many other functions for Flows like take() where you can pass the amount of items from Flow you want to test without needing to use a Job.

If you’re here, means you’ve read it till the end, so thank you for your time and hope it will be useful! To learn more about Flow and for better understanding how it works, there are amazing articles that helped me a lot preparing my application for transition:

Written By

Fedor Erofeev, Mid-Level Android Developer @ Seven Peaks Software

Click here to follow Fedor on Medium!

Interested to be a part of our talented team?

Check the available positions and apply now!

Join our team!

Sign up today & start getting tech news.

Get the latest tech trends directly in your inbox each month. And get invited to exclusive events.