Don’t use LiveData in Repositories
We recently joined a new project with heavy LiveData
usage, everywhere. The search for main thread blockages led us down a rabbit hole of removing a significant portion of our LiveData
usages.
Our Setup
We started off with Repositories, ViewModels and Fragments. The repositories would connect to data sources and expose LiveData
. The ViewModels would then expose the LiveData
to the UI Layer.

Now — there are some issues with this, like the actually unneeded abstraction — but we won’t look at that here. Here’s what a typical Repository would look like:
From there, we used it in ViewModels:
The code didn’t look too great, but still okay. There is one big issue here though — and it’s not visible on first sight.
handleLiveData
was a simple helper function that took a LiveData
and emitted a loading state before the actual data was emitted. But at some point, we needed to run some transformations on the LiveData
. Creating a MediatorLiveData
every time is quite verbose, but luckily, there is the Transformations
package for LiveData
. It offers methods like map
and switchMap
for LiveData
.
LiveData and Threading

Soon after joining the project, we realised that we had a lot of Main Thread blocks in our app, so we went to investigate. Looking at our code and looking at LiveData
documentation, we learned that LiveData
observers are always called on the main thread.
This can be very helpful since LiveData
is often used in the UI Layer. Whenever a new value comes through, you probably want to be on the main thread anyways to display that data. But this threading behavior also shot us in the foot.
The Transformation Methods
Looking at the code underneath, our map
function looked like this:
Coming back to our threading behavior, the callback on line 4 above is called whenever the source
emits a value. In the callback, we assign the MediatorLiveData
‘s value to the result of the mapper
lambda.
Since the callback simply is a LiveData
observer, it gets called on the main thread as well. Without actually noticing, we just switched threads! That’s not exactly a great thing: If you are on the main thread, you don’t want to run any long-running operations, if you are on a background thread, you don’t want to do any UI mutations.
So in this example, our mapper
lambda gets invoked on the main thread. Just doing some data transformations might not be noticeable, but what if you’re running some more complicated operations? Maybe reading something from the database?
So we learned to be extremely cautious about threading when running transformations on LiveData
. We fixed it afterwards — you can learn how in the next part of this post, coming soon.
How this even happened
Ideally, this wouldn’t have happened in the first place. The LiveData
documentation clearly states that observers are called on the main thread, so that would have been a great indicator for the previous developer. But sometimes, time-pressure is high, or you might miss something. It happens to everyone and I’m sure there are many codebases out there that look worse than ours.
The Guide to App Architecture also recommends using LiveData
in Repositories. Lots of developers will have followed this advice, possibly bringing main thread blockages to their apps.
The other mechanism that should have helped catch this are the Support Annotations. The methods provided by the Transformations
package are annotated with @MainThread
, which tells Android Studio that that method should only be called from the main thread. Similarly, there’s also @WorkerThread
and some more.
Android Studio will complain if you’re calling a function/method/class that’s also annotated with a Thread Annotation, like this one:
In our case, those annotations weren’t used, so Android Studio didn’t have a chance to catch these mistakes. If you’re doing something slightly funky with threading, or you are unsure about it, it definitely makes sense to use the Thread Annotations to make things more explicit.
Coming back to the methods offered by the Transformations
package, I believe that it shouldn’t call the transformers on the main thread in any case. Transformations most probably shouldn’t be happening in the UI layer in any case, so running them on the main thread doesn’t make lots of sense.
A (quick) attempt at fixing the issue
So we needed a quick fix, because who wants to ship apps with main thread blockages?
A really quick fix would look something like this:
We make sure that we call the mapper
on a background thread and then set the value from the main thread. This might seem a bit complicated but LiveData#setValue
has to be called from the main thread. Alternatively, we could use LiveData#postValue
, but this might lead to unexpected behavior like values being swallowed.
Obviously, this is all not great, but it works as a quick fix to help with some main thread blocks.
LiveData and Repositories
Apart from the other points, LiveData
is tied to the Android Lifecycle. This can be a great thing, especially when communicating between ViewModel and components like Activities or Fragments.
The way LiveData
is architected, observing it mostly makes sense from the UI layer. Even when using LiveData#observeForever
which isn’t bound to a lifecycle, the observer is called on the main thread, so that every time you would have to make sure you’re on the right thread for what you want to do.
Repositories should be kept free of framework dependencies, and with that also LiveData
since it is tied to the lifecycle.
Flow to the rescue!
Coroutines Flow are an awesome alternative here. Flow
s offer similar functionality: Builders, cold streams, and useful helpers for e.g. transforming data. Unlike LiveData
, they are not bound to the lifecycle and offer more control over the execution context, which Roman Elizarov wrote a great post about.

Concluding, threading is always something to really look out for. Try to make it explicit which thread you are and should be on, e.g. by using the Thread Annotations.
The “real” culprit here is LiveData
though. Not everybody might be aware of the fact that LiveData
observers are always called on the main thread, so it is something to make sure you’re good with when using LiveData
.
With that, calling data transformation methods on the main thread is probably unexpected behavior for most developers and should be made more aware of (which this post hopefully helps with).
I’d recommend using LiveData
only for communication between ViewModels and the UI Layer where observers being called on the main thread makes more sense.
Watch this space for the second part — migrating from LiveData
to Coroutines and Flow!