Converting LiveData to Flow: More lessons learned
Android KTX provides opinionated methods facilitating using Kotlin Coroutines in Android Architecture Components. For instance, Flow<T>.asLiveData()
is quite helpful to migrate your apps using LiveData towards coroutine. Make sure to understand their behaviors underneath before using them!
This story is a continuation of the previous one.
Lesson III: Unfinished coroutines during unit tests
When converting from Flow to LiveData, the source stream might be a hot StateFlow like above. Running the test will actually end up with the following error:
kotlinx.coroutines.test.UncompletedCoroutinesError: Unfinished coroutines during teardown. Ensure all coroutines are completed or cancelled by your test.
We know a hot Flow like SharedFlow never completes, so this error appears reasonable. A common way to test hot flow is to cancel the coroutine that the hot flow is executed on. In this case, we may want to pass a custom coroutineContext to asLiveData(context = …)
and cancel the job when the assertions are done:
However, it does not work! Let’s take a step back and consult the 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).
Since we are converting a hot flow to LiveData, we will always meet the condition. The flow collection will be canceled after timeoutInMs (default is 5 seconds).
After digging it a bit more into the source code, we find that the cancellation is implemented as a delay()
call on Dispatchers.Main
:
The reason why cancel()
runs on Dispatchers.Main
is because it is invoked byLiveData.onInactive()
, where LiveData
can only be operated on the main thread.
Fix: With this finding that we are waiting for this delay()
call on Dispatcher.Main
, the solution becomes obvious: We will need to advance until idle through TestDispatcher.runBlockingTest
Lesson IV: Recollection after timeouts
After a cancellation, if the [LiveData] becomes active again, the upstream flow collection will be re-executed.
Re-execution on a LiveData converted from a hot flow should not be surprising. When the LiveData goes through an inactive-active cycle, the replay cache of a SharedFlow or the current value of a StateFlow will be observed again. In other words, the latest value will simply be replayed. And this is consistent with the behavior of LiveData.
An exception to this behavior is that observers also receive an update when they change from an inactive to an active state. (From LiveData Overview)
There is a nuance, however, that if the hot flow has downstream flows, we may encounter unexpected flow re-execution. For example:
This test tries to verify in the following scenario: A user comes to a page, leaves the page, and comes back to the same page after 5 seconds. Accordingly, LiveData will go through a full active-inactive-active cycle. When the LiveData is observed for the second time, we can tell that flowOf(it, it * 10)
is re-executed that another pair of (1, 10)
is appended to the previous observe history.
Why so? Along with the hot StateFlow’s current value being replayed, flatMapConcat
is collected again accordingly. In real apps, if we send network request through flatMapConcat
, or heavy computation work through other flow operators, they will be re-executed which might be surprising to you and waste resources if they are not the desired behavior.
Fix: To avoid re-execution after the inactive-active cycle and cancellation timeout, the final downstream flow must be a hot flow.
Take away
Flow<T>.asLiveData()
is a convenient method with opinionated implications. Please read through its documentation and be aware of whether these implications are the desired behavior:
- Flow is bound to the lifecycle of LiveData. This may indicate a performance limitation for eager data loading.
- The LiveData cooperates with cancellation and remembers if the flow was completed or canceled. This may lead to flow re-execution after activity recreation.
- When the source is a hot Flow, after inactive-active cycle and cancellation, the hot flow’s value will always be replayed. If the hot flow has a downstream flow, the downstream flow collection will be re-executed.
- Converting a hot Flow to LiveData may require particular tinkering in unit tests.