This context provides tips and best practices for avoiding common observability problems when working with LiveData and Transformations in Android development, focusing on unit testing and observing LiveData.
Abstract
This article discusses common issues encountered when working with LiveData and Transformations in Android development, particularly focusing on unit testing and observing LiveData. It highlights that a LiveData that is not observed won't emit updates and emphasizes the importance of observing the result of a transformation. The article provides an example of a failing test due to not observing the LiveData and offers solutions using observeForever() and creating a custom test helper. It also mentions the InstantTaskExecutorRule for addressing threading issues and discusses potential problems with using the wrong ViewModel or Room database instance.
Bullet points
LiveData that is not observed won't emit updates.
The result of a LiveData transformation must be observed for the transformation to be evaluated.
Unit tests with LiveData may fail if the LiveData is not observed.
Using observeForever() can help fix this issue, but it can clutter tests with unnecessary statements.
Creating a custom test helper to observe a LiveData until it receives a new value is a better solution.
The InstantTaskExecutorRule can help solve threading issues in unit tests.
Using the wrong ViewModel or Room database instance can cause LiveData not to emit updates.
Multi-instance invalidation can be enabled in the Room database builder, but it has limitations.
The LiveDataTestUtil in the LiveDataSample demonstrates these concepts in action.
Unit-testing LiveData and other common observability problems
Next time you’re scratching your head wondering why an innocent looking unit test with LiveDatas is failing, or staring at an empty screen that should show… something, you should remember that a LiveData that is not observed won’t emit updates. In this post you’ll learn some good practices to avoid this problem.
This is especially important when dealing with Transformations. If a LiveData is transformed, the result of the transformation must be observed. Otherwise the transformation will never be evaluated.
If a LiveData falls in a forest and no one is observing it… was it ever updated?
In this example, the value of the initial _liveData1 is transformed (mapped) to upper case whenever the initial value changes:
Let’s write a simple test for it:
This test fails because LiveData doesn’t do more work than needed. Reading liveData2.valuedoesn’t initiate the chain of dependent transformations because it’s not observed. Only a subscription through observe() does that.
The result of a Transformation is not computed if it’s not observed
Note that liveData1’s value can be read because it’s a MutableLiveData and no transformations need to be evaluated.
A simple (but not great) way to fix this issue would be to call observeForever() on the LiveDatas that we need to read:
The result of a Transformation is not computed if it’s not observed
However if we need to observe multiple LiveDatas this can fill our tests quickly with unimportant statements.
To fix this, Jetpack doesn’t provide a test helper yet, but we can make our own:
This function observes a LiveData until it receives a new value (via onChanged) and then it removes the observer. If the LiveData already has a value, it returns it immediately. Additionally, if the value is never set, it will throw an exception after 2 seconds (or whatever you set). This prevents tests that never finish when something goes wrong.
Alternatively, if you need to keep observing a LiveData throughout a test, because it might receive multiple values that you want to check, take a look at observeForTestingin the LiveData sample.
Now the test is much more readable:
Note: While you don’t need to do this with MutableLiveDatas, it’s good practice to getOrAwaitValue when reading any LiveData value in case the implementation detail changes in the future:
You don’t need to observe MutableLiveDatas to get their value, but it’s a good practice.
InstantTaskExecutorRule
It’s important to note that this technique can have threading issues. Most of them can be solved by adding InstantTaskExecutorRule to your unit tests. However, if you call LiveData.postValue() from the main thread, the documented precedence might not be preserved. This is not common but worth mentioning because it can create evasive bugs.
If you’re using a shared ViewModel between multiple fragments, make sure you’re using the same instance in all screens. This can happen when passing the Fragment instead of the Activity as the LifecycleOwner to the ViewModelProviders or using by ViewModels in a fragment instead of by activityViewModels().
Wrong Room database instance
Why is a query that returns a LiveData not emitting any updates?
If you’re sure that the LiveData is being observed, you might be using different instances of the database. Check your creation patterns or your DI graph. If you know what you’re doing, you can also enable multi-instance invalidation in the database builder, but it has some limitations.