Converting LiveData to Flow: Lessons learned

Photo: Robert Lukeman from Unsplash

Android KTX provides opinionated methods, such asFlow<T>.asLiveData(), facilitating using Kotlin Coroutines in Android Architecture Components. Make sure to understand their behaviors underneath before using them!

The official documentation on Kotlin coroutines on Android is quitewell-written, while there are numerous article posts covering the basics. In this story, we will focus on the lessons learned when using these extension methods, usingFlow<T>.asLiveData() as an example.

Setup

Let’s start with a concrete example using the orthodox Android App Architecture: Repository + ViewModel + Fragment

Repository Layer
  • RedditService is a simple Retrofit service.
  • We also added logging statements inside the flow collecting block.
ViewModel Layer
  • We use Flow<T>.asLiveData() to convert the flow into a LiveData.
Fragment Layer
  • In the fragment layer, we added logging statements for the fragment lifecycle calls and used Thread.sleep(500)to simulate complex layout inflation.

Lesson I: Flow collection is re-executed after rotating the screen

When we run the app, it looks fine. However, after rotating the device screen to recreate the fragment, we can observe more logs like flow running... indicating the flow is executed.

2021-04-02 16:41:32.246 I: Fragment onCreate...
2021-04-02 16:41:32.247 I: Fragment onCreateView...
2021-04-02 16:41:32.754 I: Fragment onStart...
2021-04-02 16:41:32.755 I: flow running...
2021-04-02 16:41:32.756 I: Fragment onResume...
2021-04-02 16:41:33.309 I: Observer called...

That is weird! The original intention of ViewModel is to persist data that can survive UI recreation. After we rotate the device screen, the data should be retrieved from the storage. Contrarily, what we saw is that the data is fetched again from the service, which may cause unnecessary network calls or UI flicker.

Now let’s take a look at the method documentation of Flow<T>.asLiveData() :

...If the LiveData becomes inactive (LiveData.onInactive) while the flow has not completed, the flow collection will be cancelled after timeoutInMs milliseconds unless the LiveData becomes active again before that timeout (to gracefully handle cases like Activity rotation).After a cancellation, if the LiveData becomes active again, the upstream flow collection will be re-executed.If the upstream flow completes successfully or is cancelled due to reasons other than LiveData becoming inactive, it will not be re-collected even after LiveData goes through active inactive cycle.

Our flow should complete successfully before rotation. This means that it will not be recollected even after LiveData goes through active-inactive cycle. But why do we still observe recollection?

The bug in our code is that the LiveData is not preserved as a property. If we create a new LiveData instance every time, the new LiveData will not know that the previous flow collection completes successfully.

Fix: remove get()

val reddits: LiveData<RedditListing>
get() = redditRepository.topReddits.asLiveData(context = Dispatchers.IO)

to

val reddits: LiveData<RedditListing> =
redditRepository.topReddits.asLiveData(context = Dispatchers.IO)

Learning II: Flow collection starts after onStart()

If we inspect the logcat again, we can see that flow collection does not start until onStart(). This is inconsistent with what we observe in onCreateView() , why is it so?

Let’s go back to the method documentation again:

Creates a LiveData that has values collected from the origin Flow.The upstream flow collection starts when the returned LiveData becomes active (LiveData.onActive).

Since LiveData is triggered when the lifecycle owner enters the STARTED state, this behavior is consistent with the Android Architecture Components.

Time series diagram using Flow.asLiveData()

If we want to eagerly trigger the flow collection before onStart(), then we probably need to leverage viewModelScope extension to launch the flow. With this, the flow collection could be brought ahead of time as early as ViewModel creation.

Fix: use viewModelScope

Now without the KTX extension methods, we need to manually update the LiveData inside Flow<T>.collect() function.

ViewModel Layer using viewModelScope

With this change, we can move the flow collection to when we initialize the ViewModel (In our example, in onCreateView()).

Time series diagram using ViewModelScope

Take away

Flow<T>.asLiveData() is a convenient method with opinionated implications:

  • It cooperates with cancellation and remembers if the flow was completed or canceled. This may lead to trivial bugs like not preserving the Flow execution after activity recreation.
  • It binds Flow to the lifecycle of LiveData. This may indicate a performance limitation for eager data loading.

Android Developer@LinkedIn

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store