Refactoring from LiveData to Coroutines & Flow

Jossi Wolf
5 min readAug 24, 2020

If you haven’t, I recommend reading my other post about LiveData in Repositories.

We recently joined a new project with heavy LiveData usage, everywhere. The search for main thread blockages led us down a rabbit hole of removing a significant portion of our LiveData usages. Here’s how we migrated our Repositories from LiveData.

We were faced with a tough challenge: We had to migrate as quickly and smoothly as possible — there was no time for huge interruptions.
Having all our repositories and lots of helper classes use LiveData, this left us with two options:

  1. Make sure we get threading right in all places that use LiveData
  2. Replace LiveData in our Repositories

Making sure we’re on the right thread everywhere and continuing to use LiveData would be the solution requiring fewer changes, but we would still be using LiveData for a use case it isn’t really designed for.

The Android Developer Advocacy Team agrees that this isn’t the best use case ever for LiveData :)

Our code was also quite convoluted and hard to fix, so we decided to look for another option to handle asynchronicity. In an ideal world, refactoring it would have been easier, and we probably would have stuck with LiveData had our code been less convoluted.

Our main requirement was for our replacement to fit with our existing mental model — ViewModels being able to observe data from repositories where needed.

Looking at our code, we were able to categorise our LiveData use cases into two categories:

  • One-shot LiveData, for example, network calls
  • Streams of data (e.g. for retrieving cached data before emitting fresh data)

Our Replacement

We looked around for a bit and decided to go with Coroutines and Flow. We had used both in other projects successfully before and with the Android Team now embracing Coroutines officially, we decided to give it a shot! After a bit of experimenting, we found that Coroutines and Flow would perfectly fit our use case. Let’s take a look at how we migrated!

One-Shot Cases

In a lot of places, we were just running a network call and returning the value, like in our first example.

The actual first step was to get rid of callbacks. Now, this isn’t an actual requirement for migrating from LiveData, but using Coroutines enabled us getting rid of a lot of unneeded code.

Our fetchOpeningHours method after moving our network interface to Coroutines. Thanks, Retrofit!

Looking at the actual logic in this function, there’s not a lot going on — we’re only calling a method in Line 8. Everything else is related to creating the LiveData. That’s a great place to start extracting!

To keep our repositories compatible, we introduced Migration Helpers. These provide code compatible with our old code while enabling us to have cleaner code and delete the migration helpers over time.

We leveraged two awesome features here:

The LiveData Builder Lambda

The liveData builder lambda is a helper method provided by the androidx.lifecycle:livedata-ktx artefact. It lets you build a LiveData easily.
On top of that, you can also specify the execution context of the builder lambda (Line 8 in the sample above) through the context parameter. This is executed on Dispatchers.Main , so for our use case, it was especially important to always set this.

@Deprecated

Finally, we annotated the migration helper with the @Deprecated annotation. This means that any code using this function gets a visual hint in the IDE.

What using deprecated code looks like in our ViewModel.

This solved already some problems in our repositories, but still had issues with our ViewModels where we were transforming values from LiveData.

Even with our shiny repository, the lambda in Line 7 would still be called on the main thread. So our next step was moving our migration helpers from the repository to the ViewModel.

With this, we got rid of the transformation methods. This improved code readability and we could be sure about execution context. Yay!

After some time, we removed the left-over migration helpers one-by-one.

Multi-Event Cases

In some rarer cases, we had LiveData emitting a locally stored value while running a network call and then emitting the fresh value.

We decided to replace these cases with Coroutines Flow. For that, we essentially used the same strategy that had served us well for migrating to Coroutines:

  1. Extract the logic to functions (as you should anyway :) )
  2. Provide migration helpers

The androidx.livedata:lifecycle-ktx artefact also provides some extension functions for LiveData <> Flow interop which we used in order not to break our ViewModels.

From there, we could slowly migrate our ViewModels to use the Flow implementations instead.

With Flow, execution context works differently to LiveData and is easier to control. Roman Elizarov wrote a great post about this. Now we could easily run any data transformations that we needed, without any danger.

Flows! Photo by v2osk on Unsplash

Our learnings

All in all, migrating wasn’t easy — there were still lots of pain points and issues that we had to fix. It was worth it in the long run though, and after migrating, we didn’t have any issues anymore.

Using the helpers from livedata-ktx and awesome tools like @Deprecated really made our lives easier as well. If you are considering to migrate from LiveData to Coroutines, Flow or something different, be aware that it takes time, but the process can be smoothened out and go can migrate piece by piece, at the pace fitting for your codebase.

Thanks for reading! Let me know what your thoughts and experiences migrating to Flow and Coroutines are.

I gave a talk about this topic recently. You can find the slides here and the recording here.

--

--

Jossi Wolf

Software Engineer @Google working on Jetpack Compose. I like Kotlin, Animations and Rabbits! speakerdeck.com/jossiwolf