Android KTX provides opinionated methods, such as
Flow<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, using
Flow<T>.asLiveData() as an example.
Let’s start with a concrete example using the orthodox Android App Architecture: Repository + ViewModel + Fragment
RedditServiceis 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
...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.
val reddits: LiveData<RedditListing>
get() = redditRepository.topReddits.asLiveData(context = Dispatchers.IO)
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
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).
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
With this change, we can move the flow collection to when we initialize the ViewModel (In our example, in onCreateView()).
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.