Converting LiveData to Flow: Lessons learned
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
RedditService
is a simple Retrofit service.- We also added logging statements inside the flow collecting block.
- We use
Flow<T>.asLiveData()
to convert the flow into a LiveData.
- 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.
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.
With this change, we can move the flow collection to when we initialize the ViewModel (In our example, in onCreateView()).