Merge LiveData like you need it

How to merge multiple LiveData of any type into one LiveData.

Daniel Knauf
Nerd For Tech

--

credit: clarocity

While working with LiveData in several projects, I faced recurring challenges related to MediatorLiveData:

  • It allows to add multiple sources one after another, but none of the sources know the value of the other ones. If a change occurs in one source, not all source values can be evaluated in interaction with each other to determine the new value.
  • It cannot be instantiated with an initial source and mapping.

After copying code blocks from one project to another — changing classes to methods and back again — I ended up with the solution presented here. The goal was to have a generic solution, which could be easily integrated and fitted to every use case — code once, use anywhere.

MergerLiveData, an extension to MediatorLiveData, solves the challenge of transforming one or multiple source types into one target type:

A MergerLiveData subscribes to changes of one or more sources when the first observer is attached as well as removes all sources when the last observer detaches. By providing a merging function, you can easily transform any source types into any target type.
Only non-null values are emitted into the merging function to keep execution as safe as possible. The merge result is emitted onto the main thread asynchronously by using the postValue(T), allowing to use MergerLiveData in background services as well. DistinctUntilChanged regulates if all values are posted or only if they differ from the current one.

By using a sealed class, all implementations are grouped together under one root and IDE autocomplete can be utilized best — just type in MergerLiveData. and all types are displayed.
Although in case of MergerLiveData.One it is a one-to-one mapping and the class should therefore be called MappingLiveData. This special case remains inside the MergerLiveData group to keep consistency and ease of use as high as possible. Its transformation function is called mapping. This might look inconsistent, but with trailing lambdas the function is mostly not passed as a (named) parameter anyway and so the function name reflects the performed action.

A use case for MergerLiveData is observing multiple LiveData inputs of a contact form and enable a submit button if certain conditions are met.

Without the use of MergerLiveData, the validation is performed within the layout and therefore the business logic is split between the ViewModel and the layout. Additionally combined boolean expressions are not very readable within the XML file.

By introducing a MergerLiveData.Two, the boolean expression can be shifted into the ViewModel. Thus all business logic is pooled into one class and not spitted across two files. As a developer, you only have to look inside the ViewModel which becomes the single point of truth. In addition, complex boolean statements can be described more readable by separating them into different methods or variables.
Finally, MergerLiveData eases the creation of MediatorLiveData by enabling to set an initial source and mapping right away — without having to use the common workaround utilising apply():

val mediatorLiveData = MediatorLiveData<T>().apply { 
addSource(source) { value ->
// do stuff
}
}

Other use cases for MergerLiveData could be:

  • transforming multiple local (e.g. using Room) or remote data sources into one UI model
  • merging multiple sources to one Composable representing the current UI state

MergerLiveData is part of LiveData-Kit, which can be easily included into a project via JitPack. You find all necessary lines of code on GitHub.

Thanks for reading!

--

--

Daniel Knauf
Nerd For Tech

Education researcher turned Android developer — exploring the vast world of coding.