MediatorLiveData or switchMap transformation with multiple parameters - android
I am using Transformations.switchMap in my ViewModel so my LiveData collection, observed in my fragment, reacts on changes of code parameter.
This works perfectly :
public class MyViewModel extends AndroidViewModel {
private final LiveData<DayPrices> dayPrices;
private final MutableLiveData<String> code = new MutableLiveData<>();
// private final MutableLiveData<Integer> nbDays = new MutableLiveData<>();
private final DBManager dbManager;
public MyViewModel(Application application) {
super(application);
dbManager = new DBManager(application.getApplicationContext());
dayPrices = Transformations.switchMap(
code,
value -> dbManager.getDayPriceData(value/*, nbDays*/)
);
}
public LiveData<DayPrices> getDayPrices() {
return dayPrices;
}
public void setCode(String code) {
this.code.setValue(code);
}
/*public void setNbDays(int nbDays) {
this.nbDays.setValue(nbDays);
}*/
}
public class MyFragment extends Fragment {
private MyViewModel myViewModel;
myViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
myViewModel.setCode("SO");
//myViewModel.setNbDays(30);
myViewModel.getDayPrices().observe(MyFragment.this, dataList -> {
// update UI with data from dataList
});
}
Problem
I now need another parameter (nbDays commented in the code above), so that my LiveData object reacts on both parameters change (code and nbDays).
How can I chain transformations ?
Some reading pointed me to MediatorLiveData, but it does not solve my problem (still need to call single DB function with 2 parameters, I don't need to merge 2 liveDatas).
So I tried this instead of switchMap but code and nbDays are always null.
dayPrices.addSource(
dbManager.getDayPriceData(code.getValue(), nbDays.getValue),
apiResponse -> dayPrices.setValue(apiResponse)
);
One solution would be to pass an object as single parameter by I'm pretty sure there is a simple solution to this.
Source : https://plus.google.com/+MichielPijnackerHordijk/posts/QGXF9gRomVi
To have multiple triggers for switchMap(), you need to use a custom MediatorLiveData to observe the combination of the LiveData objects -
class CustomLiveData extends MediatorLiveData<Pair<String, Integer>> {
public CustomLiveData(LiveData<String> code, LiveData<Integer> nbDays) {
addSource(code, new Observer<String>() {
public void onChanged(#Nullable String first) {
setValue(Pair.create(first, nbDays.getValue()));
}
});
addSource(nbDays, new Observer<Integer>() {
public void onChanged(#Nullable Integer second) {
setValue(Pair.create(code.getValue(), second));
}
});
}
}
Then you can do this -
CustomLiveData trigger = new CustomLiveData(code, nbDays);
LiveData<DayPrices> dayPrices = Transformations.switchMap(trigger,
value -> dbManager.getDayPriceData(value.first, value.second));
If you use Kotlin and want to work with generics:
class DoubleTrigger<A, B>(a: LiveData<A>, b: LiveData<B>) : MediatorLiveData<Pair<A?, B?>>() {
init {
addSource(a) { value = it to b.value }
addSource(b) { value = a.value to it }
}
}
Then:
val dayPrices = Transformations.switchMap(DoubleTrigger(code, nbDays)) {
dbManager.getDayPriceData(it.first, it.second)
}
Custom MediatorLiveData as proposed by #jL4 works great and is probably the solution.
I just wanted to share the simplest solution that I think is to use an inner class to represent the composed filter values :
public class MyViewModel extends AndroidViewModel {
private final LiveData<DayPrices> dayPrices;
private final DBManager dbManager;
private final MutableLiveData<DayPriceFilter> dayPriceFilter;
public MyViewModel(Application application) {
super(application);
dbManager = new DBManager(application.getApplicationContext());
dayPriceFilter = new MutableLiveData<>();
dayPrices = Transformations.switchMap(dayPriceFilter, input -> dbManager.getDayPriceData(input.code, input.nbDays));
}
public LiveData<DayPrices> getDayPrices() {
return dayPrices;
}
public void setDayPriceFilter(String code, int nbDays) {
DayPriceFilter update = new DayPriceFilter(code, nbDays);
if (Objects.equals(dayPriceFilter.getValue(), update)) {
return;
}
dayPriceFilter.setValue(update);
}
static class DayPriceFilter {
final String code;
final int nbDays;
DayPriceFilter(String code, int nbDays) {
this.code = code == null ? null : code.trim();
this.nbDays = nbDays;
}
}
}
Then in the activity/fragment :
public class MyFragment extends Fragment {
private MyViewModel myViewModel;
myViewModel = ViewModelProviders.of(this).get(MyViewModel.class);
myViewModel.setDayPriceFilter("SO", 365);
myViewModel.getDayPrices().observe(MyFragment.this, dataList -> {
// update UI with data from dataList
});
}
A simplification of jL4's answer, (and also in Kotlin in case it helps anybody)... no need to create a custom class for this:
class YourViewModel: ViewModel() {
val firstLiveData: LiveData<String> // or whatever type
val secondLiveData: LiveData<Int> // or whatever
// the Pair values are nullable as getting "liveData.value" can be null
val combinedValues = MediatorLiveData<Pair<String?, Int?>>().apply {
addSource(firstLiveData) {
value = Pair(it, secondLiveData.value)
}
addSource(secondLiveData) {
value = Pair(firstLiveData.value, it)
}
}
val results = Transformations.switchMap(combinedValues) { pair ->
val firstValue = pair.first
val secondValue = pair.second
if (firstValue != null && secondValue != null) {
yourDataSource.yourLiveDataCall(firstValue, secondValue)
} else null
}
}
Explanation
Any update in firstLiveData or secondLiveData will update the value of combinedValues, and emit the two values as a pair (thanks to jL4 for this).
Calling liveData.value can be null, so this solution makes the values in Pair nullable to avoid Null Pointer Exception.
So for the actual results/datasource call, the switch map is on the combinedValues live data, and the 2 values are extracted from the Pair and null checks are performed, so you can be sure of passing non-null values to your data source.
I use following classes to transform many live data with different types
class MultiMapLiveData<T>(
private val liveDataSources: Array<LiveData<*>>,
private val waitFirstValues: Boolean = true,
private val transform: (signalledLiveData: LiveData<*>) -> T
): LiveData<T>() {
private val mObservers = ArrayList<Observer<Any>>()
private var mInitializedSources = mutableSetOf<LiveData<*>>()
override fun onActive() {
super.onActive()
if (mObservers.isNotEmpty()) throw InternalError(REACTIVATION_ERROR_MESSAGE)
if (mInitializedSources.isNotEmpty()) throw InternalError(REACTIVATION_ERROR_MESSAGE)
for (t in liveDataSources.indices) {
val liveDataSource = liveDataSources[t]
val observer = Observer<Any> {
if (waitFirstValues) {
if (mInitializedSources.size < liveDataSources.size) {
mInitializedSources.add(liveDataSource)
}
if (mInitializedSources.size == liveDataSources.size) {
value = transform(liveDataSource)
}
} else {
value = transform(liveDataSource)
}
}
liveDataSource.observeForever(observer)
mObservers.add(observer)
}
}
override fun onInactive() {
super.onInactive()
for (t in liveDataSources.indices) {
val liveDataSource = liveDataSources[t]
val observer = mObservers[t]
liveDataSource.removeObserver(observer)
}
mObservers.clear()
mInitializedSources.clear()
}
companion object {
private const val REACTIVATION_ERROR_MESSAGE = "Reactivation of active LiveData"
}
}
class MyTransformations {
companion object {
fun <T> multiMap(
liveDataSources: Array<LiveData<*>>,
waitFirstValues: Boolean = true,
transform: (signalledLiveData: LiveData<*>) -> T
): LiveData<T> {
return MultiMapLiveData(liveDataSources, waitFirstValues, transform)
}
fun <T> multiSwitch(
liveDataSources: Array<LiveData<*>>,
waitFirstValues: Boolean = true,
transform: (signalledLiveData: LiveData<*>) -> LiveData<T>
): LiveData<T> {
return Transformations.switchMap(
multiMap(liveDataSources, waitFirstValues) {
transform(it)
}) {
it
}
}
}
}
Usage:
Note that the logic of the work is slightly different. The LiveData that caused the update (signalledLiveData) is passed to the Tranformation Listener as parameter, NOT the values of all LiveData. You get the current LiveData values yourself in the usual way via value property.
examples:
class SequenceLiveData(
scope: CoroutineScope,
start: Int,
step: Int,
times: Int
): LiveData<Int>(start) {
private var current = start
init {
scope.launch {
repeat (times) {
value = current
current += step
delay(1000)
}
}
}
}
suspend fun testMultiMap(lifecycleOwner: LifecycleOwner, scope: CoroutineScope) {
val liveS = MutableLiveData<String>("aaa")
val liveI = MutableLiveData<Int>()
val liveB = MutableLiveData<Boolean>()
val multiLiveWait: LiveData<String> = MyTransformations.multiMap(arrayOf(liveS, liveI, liveB)) {
when (it) {
liveS -> log("liveS changed")
liveI -> log("liveI changed")
liveB -> log("liveB changed")
}
"multiLiveWait: S = ${liveS.value}, I = ${liveI.value}, B = ${liveB.value}"
}
val multiLiveNoWait: LiveData<String> = MyTransformations.multiMap(arrayOf(liveS, liveI, liveB), false) {
when (it) {
liveS -> log("liveS changed")
liveI -> log("liveI changed")
liveB -> log("liveB changed")
}
"multiLiveNoWait: S = ${liveS.value}, I = ${liveI.value}, B = ${liveB.value}"
}
multiLiveWait.observe(lifecycleOwner) {
log(it)
}
multiLiveNoWait.observe(lifecycleOwner) {
log(it)
}
scope.launch {
delay(1000)
liveS.value = "bbb"
delay(1000)
liveI.value = 2222
delay(1000)
liveB.value = true // ***
delay(1000)
liveI.value = 3333
// multiLiveWait generates:
//
// <-- waits until all sources get first values (***)
//
// liveB changed: S = bbb, I = 2222, B = true
// liveI changed: S = bbb, I = 3333, B = true
// multiLiveNoWait generates:
// liveS changed: S = aaa, I = null, B = null
// liveS changed: S = bbb, I = null, B = null
// liveI changed: S = bbb, I = 2222, B = null
// liveB changed: S = bbb, I = 2222, B = true <-- ***
// liveI changed: S = bbb, I = 3333, B = true
}
}
suspend fun testMultiMapSwitch(lifecycleOwner: LifecycleOwner, scope: CoroutineScope) {
scope.launch {
val start1 = MutableLiveData(0)
val step1 = MutableLiveData(1)
val multiLiveData = MyTransformations.multiSwitch(arrayOf(start1, step1)) {
SequenceLiveData(scope, start1.value!!, step1.value!!, 5)
}
multiLiveData.observe(lifecycleOwner) {
log("$it")
}
delay(7000)
start1.value = 100
step1.value = 2
delay(7000)
start1.value = 200
step1.value = 3
delay(7000)
// generates:
// 0
// 1
// 2
// 3
// 4
// 100 <-- start.value = 100
// 100 <-- step.value = 2
// 102
// 104
// 106
// 108
// 200 <-- start.value = 200
// 200 <-- step.value = 3
// 203
// 206
// 209
// 212
}
}
I faced a similar problem. There are 2 ways to solve this:
Either use MediatorLiveData
Use RxJava as it has various operators to do such kind of complex stuff
If you don't know RxJava, then I'd recommend writing your custom MediatorLiveData class.
To learn how write custom MediatorLiveData class check out this example:
https://gist.github.com/AkshayChordiya/a79bfcc422fd27d52b15cdafc55eac6b
Related
How to test ViewModel + Flow
I'm doing a small project to learn flow and the latest Android features, and I'm currently facing the viewModel's testing, which I don't know if I'm performing correctly. can you help me with it? Currently, I am using a use case to call the repository which calls a remote data source that gets from an API service a list of strings. I have created a State to control the values in the view model: data class StringItemsState( val isLoading: Boolean = false, val items: List<String> = emptyList(), val error: String = "" ) and the flow: private val stringItemsState = StringtemsState() private val _stateFlow = MutableStateFlow(stringItemsState) val stateFlow = _stateFlow.asStateFlow() and finally the method that performs all the logic in the viewModel: fun fetchStringItems() { try { _stateFlow.value = stringItemsState.copy(isLoading = true) viewModelScope.launch(Dispatchers.IO) { val result = getStringItemsUseCase.execute() if (result.isEmpty()) { _stateFlow.value = stringItemsState } else { _stateFlow.value = stringItemsState.copy(items = result) } } } catch (e: Exception) { e.localizedMessage?.let { _stateFlow.value = stringItemsState.copy(error = it) } } } I am trying to perform the test following the What / Where / Then pattern, but the result is always an empty list and the assert verification always fails: private val stringItems = listOf<String>("A", "B", "C") #Test fun `get string items - not empty`() = runBlocking { // What coEvery { useCase.execute() } returns stringItems // Where viewModel.fetchStringItems() // Then assert(viewModel.stateFlow.value.items == stringItems) coVerify(exactly = 1) { viewModel.fetchStringItems() } } Can someone help me and tell me if I am doing it correctly? Thanks.
StateFlow collect not firing for list type
#HiltViewModel class HistoryViewModel #Inject constructor(private val firebaseRepository: FirebaseRepository) : ViewModel() { private val translateList: MutableList<Translate> = mutableListOf() private val _translateListState: MutableStateFlow<List<Translate>> = MutableStateFlow(translateList) val translateListState = _translateListState.asStateFlow() init { listenToSnapshotData() } private suspend fun addItemToList(translate: Translate) { Log.d("customTag", "item added adapter $translate") translateList.add(translate) _translateListState.emit(translateList) } private suspend fun removeItemFromList(translate: Translate) { Log.d("customTag", "item removed adapter $translate") val indexOfItem = translateList.indexOfFirst { it.id == translate.id } if (indexOfItem != -1) { translateList.removeAt(indexOfItem) _translateListState.emit(translateList) } } private suspend fun updateItemFromList(translate: Translate) { Log.d("customTag", "item modified adapter $translate") val indexOfItem = translateList.indexOfFirst { it.id == translate.id } if (indexOfItem != -1) { translateList[indexOfItem] = translate _translateListState.emit(translateList) } } private fun listenToSnapshotData() { viewModelScope.launch { firebaseRepository.translateListSnapshotListener().collect { querySnapshot -> querySnapshot?.let { for (document in it.documentChanges) { val translateData = document.document.toObject(Translate::class.java) when (document.type) { DocumentChange.Type.ADDED -> { addItemToList(translate = translateData) } DocumentChange.Type.MODIFIED -> { updateItemFromList(translate = translateData) } DocumentChange.Type.REMOVED -> { removeItemFromList(translate = translateData) } } } } } } } } Here data comes properly in querySnapshot in listenToSnapshotData function. And post that it properly calls corresponding function to update the list. But after this line _translateListState.emit(translateList) flow doesn't go to corresponding collectLatest private fun observeSnapShotResponse() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { historyViewModel.translateListState.collectLatest { Log.d("customTag", "calling submitList from fragment") translateListAdapter.submitList(it) } } } } calling submitList from fragment is called once at the start, but as & when data is modified in list viewmodel, callback doesn't come to collectLatest
This is from StateFlow documentation: Values in state flow are conflated using Any.equals comparison in a similar way to distinctUntilChanged operator. It is used to conflate incoming updates to value in MutableStateFlow and to suppress emission of the values to collectors when new value is equal to the previously emitted one. You are trying to emit the same instance of List all the time, which has no effect because of what is written in the docs. You will have to create new instance of the list every time.
Unable to retrive data from database using Room and coroutine
I am not able to retrieve data. Is there any way by which we can access our database and we can check what we have inserted so far. In this code I am trying to insert the latest calculation I did in my calculator with the number of my transaction. Using Coroutines, Room and View Model. import android.app.Application import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.navigation.NavController import com.kotlin_developer.calculator.database.CalculationDatabaseDao import com.kotlin_developer.calculator.database.CalculatorHistory import kotlinx.coroutines.* import timber.log.Timber class CalculatorViewModel( val database: CalculationDatabaseDao, application: Application ) : AndroidViewModel(application) { var operatorEnabled: Boolean = false var firstResult: Double = 0.0 var operator: Char = ' ' var ifNumeric: Boolean = true var secondResultLenght = 0 // First step of coroutines is to create job, this can cancel all the coroutines started by this view model private var viewModelJob = Job() // Second step is to create the scope where we want to run our code // Scope determines what thread the coroutines will run on, it also needs to know about the job private val uiScope = CoroutineScope(Dispatchers.Main + viewModelJob) // private val history = database.getAllCalculation() private var _totalTransaction = MutableLiveData<Int>() val totalTransaction: LiveData<Int> get() = _totalTransaction //Getting currentCalculation private var _currentCalculation = MutableLiveData<String>() val currentCalculation: LiveData<String> get() = _currentCalculation //Getting current result private var _currentResult = MutableLiveData<String>() val currentResult: LiveData<String> get() = _currentResult val navControler = MutableLiveData<NavController>() private val _secondResult = MutableLiveData<Double>() val secondResult: LiveData<Double> get() = _secondResult private var _resultTextValue = MutableLiveData<Double>() val resultTextValue: LiveData<Double> get() = _resultTextValue private var _lastHistory = MutableLiveData<CalculatorHistory>() val lastHistory: LiveData<CalculatorHistory> get() = _lastHistory val totalCalculation = mutableListOf<String>() init { Timber.i("Calculator View Model created") _resultTextValue.value = 0.0 _secondResult.value = 0.0 _totalTransaction.value = 0 } fun insertData() { uiScope.launch { val newHistory = CalculatorHistory( totalTransaction.value?.toLong()!!, totalCalculation[totalTransaction.value!! - 1] ) insert(newHistory) } } private suspend fun insert(newHistory: CalculatorHistory) { withContext(Dispatchers.IO) { database.insert(newHistory) Timber.i("Data Inserted") } } internal fun initializeHistory() { uiScope.launch { _lastHistory.value = getHistoryFromDatabase() } Timber.i("${lastHistory.value?.transactionNumber} and ${lastHistory.value?.calculation}") } private suspend fun getHistoryFromDatabase(): CalculatorHistory? { return withContext(Dispatchers.IO) { var lastCalculation = database.get(1) lastCalculation } Timber.i("${lastHistory.value?.transactionNumber} and ${lastHistory.value?.calculation}") } fun calculator(operator: Char): Double { return when (operator) { '+' -> firstResult.plus(secondResult.value ?: 0.0) '-' -> firstResult.minus(secondResult.value ?: 0.0) '*' -> firstResult.times(secondResult.value ?: 1.0) '/' -> firstResult.div(secondResult.value ?: 1.0) else -> firstResult.rem(secondResult.value ?: 1.0) } } fun createCalculation() { ifNumeric = false operatorEnabled = false _resultTextValue.value = calculator(operator) //This we can use in future to create a list of calculation totalCalculation.add( totalTransaction.value!!, "${currentCalculation.value}=${_resultTextValue.value}" ) _currentResult.value = totalCalculation[totalTransaction.value!!] insertData() firstResult = _resultTextValue.value ?: 0.0 _totalTransaction.value = _totalTransaction.value?.plus(1) _secondResult.value = 0.0 secondResultLenght = 0 } fun seprateNumber(number: Double) { if (operatorEnabled) { if (ifNumeric) { _secondResult.value = number + (secondResult.value?.times(10.0))!! } else { _secondResult.value = number } } else { firstResult = number + (firstResult * 10) } } fun clearText() { _resultTextValue.value = 0.0 _currentResult.value = "" firstResult = 0.0 _secondResult.value = 0.0 secondResultLenght = 0 operator = ' ' operatorEnabled = false ifNumeric = false _currentCalculation.value = "" } fun ifSeprateNumber(number: Double) { seprateNumber(number) if (operatorEnabled) { secondCalculationText() } else { _currentCalculation.value = firstResult.toString() } ifNumeric = true } fun secondCalculationText() { _currentCalculation.value = _currentCalculation.value ?.removeRange( _currentCalculation.value!!.length - secondResultLenght, _currentCalculation.value!!.length ) _currentCalculation.value = "${_currentCalculation.value}${secondResult.value.toString()}" secondResultLenght = secondResult.value.toString().length ifNumeric = true } fun addTextToField() { ifNumeric = false operatorEnabled = true _currentCalculation.value = "${_currentCalculation.value}${operator}" } override fun onCleared() { super.onCleared() //This cancels all the coroutines when the view model is getting closed viewModelJob.cancel() Timber.i("Calculator ViewModel destroyed") } }
I think you are doing too many things in your ViewModel. If you want to see what the data in your database is, you should have an activity (the View, the activity which creates, updates and handles UI events) that observes your livedata in the viewmodel. Everytime you insert values into your database, regardless of the time it takes, once it's done it will trigger a callback to the observer and you will get the new values. I would start with this, so you can keep track of what you are inserting in your database, and it's the starting point to then using those values. Would be something like this, from your View/Activity: yourViewModel.variableYouWant.observe(this, Observer { yourValue -> //do something with yourValue })
kotlin.KotlinNullPointerException when acces through view model
I've view model with MutableLiveData that look like this: private val _num = MutableLiveData<Float>() val num: LiveData<Float> get() = _num val sum: Int= sumMap.map { it.value }.sumBy { it.num} _num.value = sum.toFloat() I'm able to get the value correctly, but when I tried to access through the viewmodel objec, I get kotlin.KotlinNullPointerException Why I'm getting this error? my code in my fragment look like this: val dVal: Float = viewModel.num.value!! Log.i("MainScreenFragment", "numVal $dVal") Also, If I remove the !! assertion, I get required Float but found Float?, Why is this? I didn't declare my variable at any point as nullable EDIT: This is the entire function, this function called inside init block private fun getPlacesCountries() { coroutineScope.launch { var getPlacesDeffers = PlaceApi.retrofitService.getPropeties() try { var result = getPlacesDeffers.await() if (result.isNotEmpty()) { val sumMap: Map<String, Data> = result.mapValues { (country,data) -> data.last() } val sum: Int= sumMap.map { it.value }.sumBy { it.num } _num.value = sum.toFloat() } } } catch (e: Exception) { _response.value = "Failure: ${e.message}" } } }
In your activity/fragment you need to observe LiveData: viewModel.num.observe(this, Observer { it?.let { chart.setData(it) } // set values if it is not null })
How to combine two live data one after the other?
I have next use case: User comes to registration form, enters name, email and password and clicks on register button. After that system needs to check if email is taken or not and based on that show error message or create new user... I am trying to do that using Room, ViewModel and LiveData. This is some project that on which I try to learn these components and I do not have remote api, I will store everything in local database So I have these classes: RegisterActivity RegisterViewModel User UsersDAO UsersRepository UsersRegistrationService So the idea that I have is that there will be listener attached to register button which will call RegisterViewModel::register() method. class RegisterViewModel extends ViewModel { //... public void register() { validationErrorMessage.setValue(null); if(!validateInput()) return; registrationService.performRegistration(name.get(), email.get(), password.get()); } //... } So that is the basic idea, I also want for performRegistration to return to me newly created user. The thing that bothers me the most is I do not know how to implement performRegistration function in the service class UsersRegistrationService { private UsersRepository usersRepo; //... public LiveData<RegistrationResponse<Parent>> performRegistration(String name, String email, String password) { // 1. check if email exists using repository // 2. if user exists return RegistrationResponse.error("Email is taken") // 3. if user does not exists create new user and return RegistrationResponse(newUser) } } As I understand, methods that are in UsersRepository should return LiveData because UsersDAO is returning LiveData #Dao abstract class UsersDAO { #Query("SELECT * FROM users WHERE email = :email LIMIT 1") abstract LiveData<User> getUserByEmail(String email); } class UsersRepository { //... public LiveData<User> findUserByEmail(String email) { return this.usersDAO.getUserByEmail(email); } } So my problem is how to implement performRegistration() function and how to pass value back to view model and then how to change activity from RegisterActivity to MainActivity...
You can use my helper method: val profile = MutableLiveData<ProfileData>() val user = MutableLiveData<CurrentUser>() val title = profile.combineWith(user) { profile, user -> "${profile.job} ${user.name}" } fun <T, K, R> LiveData<T>.combineWith( liveData: LiveData<K>, block: (T?, K?) -> R ): LiveData<R> { val result = MediatorLiveData<R>() result.addSource(this) { result.value = block(this.value, liveData.value) } result.addSource(liveData) { result.value = block(this.value, liveData.value) } return result }
With the help of MediatorLiveData, you can combine results from multiple sources. Here an example of how would I combine two sources: class CombinedLiveData<T, K, S>(source1: LiveData<T>, source2: LiveData<K>, private val combine: (data1: T?, data2: K?) -> S) : MediatorLiveData<S>() { private var data1: T? = null private var data2: K? = null init { super.addSource(source1) { data1 = it value = combine(data1, data2) } super.addSource(source2) { data2 = it value = combine(data1, data2) } } override fun <S : Any?> addSource(source: LiveData<S>, onChanged: Observer<in S>) { throw UnsupportedOperationException() } override fun <T : Any?> removeSource(toRemove: LiveData<T>) { throw UnsupportedOperationException() } } here is the gist for above, in case it is updated on the future: https://gist.github.com/guness/0a96d80bc1fb969fa70a5448aa34c215
One approach is to use flows for this. val profile = MutableLiveData<ProfileData>() val user = MutableLiveData<CurrentUser>() val titleFlow = profile.asFlow().combine(user.asFlow()){ profile, user -> "${profile.job} ${user.name}" } And then your Fragment/Activity: viewLifecycleOwner.lifecycleScope.launch { viewModel.titleFlow.collectLatest { title -> Log.d(">>", title) } } One advantage to this approach is that titleFlow will only emit value when both live datas have emitted at least one value. This interactive diagram will help you understand this https://rxmarbles.com/#combineLatest Alternative syntax: val titleFlow = combine(profile.asFlow(), user.asFlow()){ profile, user -> "${profile.job} ${user.name}" }
Jose Alcérreca has probably the best answer for this: fun blogpostBoilerplateExample(newUser: String): LiveData<UserDataResult> { val liveData1 = userOnlineDataSource.getOnlineTime(newUser) val liveData2 = userCheckinsDataSource.getCheckins(newUser) val result = MediatorLiveData<UserDataResult>() result.addSource(liveData1) { value -> result.value = combineLatestData(liveData1, liveData2) } result.addSource(liveData2) { value -> result.value = combineLatestData(liveData1, liveData2) } return result }
without custom class MediatorLiveData<Pair<Foo?, Bar?>>().apply { addSource(fooLiveData) { value = it to value?.second } addSource(barLiveData) { value = value?.first to it } }.observe(this) { pair -> // TODO }
I did an approach based on #guness answer. I found that being limited to two LiveDatas was not good. What if we want to use 3? We need to create different classes for every case. So, I created a class that handles an unlimited amount of LiveDatas. /** * CombinedLiveData is a helper class to combine results from multiple LiveData sources. * #param liveDatas Variable number of LiveData arguments. * #param combine Function reference that will be used to combine all LiveData data results. * #param R The type of data returned after combining all LiveData data. * Usage: * CombinedLiveData<SomeType>( * getLiveData1(), * getLiveData2(), * ... , * getLiveDataN() * ) { datas: List<Any?> -> * // Use datas[0], datas[1], ..., datas[N] to return a SomeType value * } */ class CombinedLiveData<R>(vararg liveDatas: LiveData<*>, private val combine: (datas: List<Any?>) -> R) : MediatorLiveData<R>() { private val datas: MutableList<Any?> = MutableList(liveDatas.size) { null } init { for(i in liveDatas.indices){ super.addSource(liveDatas[i]) { datas[i] = it value = combine(datas) } } } }
You can define a method that would combine multiple LiveDatas using a MediatorLiveData, then expose this combined result as a tuple. public class CombinedLiveData2<A, B> extends MediatorLiveData<Pair<A, B>> { private A a; private B b; public CombinedLiveData2(LiveData<A> ld1, LiveData<B> ld2) { setValue(Pair.create(a, b)); addSource(ld1, (a) -> { if(a != null) { this.a = a; } setValue(Pair.create(a, b)); }); addSource(ld2, (b) -> { if(b != null) { this.b = b; } setValue(Pair.create(a, b)); }); } } If you need more values, then you can create a CombinedLiveData3<A,B,C> and expose a Triple<A,B,C> instead of the Pair, etc. Just like in https://stackoverflow.com/a/54292960/2413303 . EDIT: hey look, I even made a library for you that does that from 2 arity up to 16: https://github.com/Zhuinden/livedata-combinetuple-kt
Many of these answers work, but also it is assumed the LiveData generic types are not-nullable. But what if one or more of the given input types are nullable types (given the default Kotlin upper bound for generics is Any?, which is nullable)? The result would be even though the LiveData emitter would emit a value (null), the MediatorLiveData will ignore it, thinking it's his own child live data value not being set. This solution, instead, takes care of it by forcing the upper bound of the types passed to the mediator to be not null. Lazy but needed. Also, this implementation avoids same-value after the combiner function has been called, which might or might not be what you need, so feel free to remove the equality check there. fun <T1 : Any, T2 : Any, R> combineLatest( liveData1: LiveData<T1>, liveData2: LiveData<T2>, combiner: (T1, T2) -> R, ): LiveData<R> = MediatorLiveData<R>().apply { var first: T1? = null var second: T2? = null fun updateValueIfNeeded() { value = combiner( first ?: return, second ?: return, )?.takeIf { it != value } ?: return } addSource(liveData1) { first = it updateValueIfNeeded() } addSource(liveData2) { second = it updateValueIfNeeded() } }
LiveData liveData1 = ...; LiveData liveData2 = ...; MediatorLiveData liveDataMerger = new MediatorLiveData<>(); liveDataMerger.addSource(liveData1, value -> liveDataMerger.setValue(value)); liveDataMerger.addSource(liveData2, value -> liveDataMerger.setValue(value));
if you want both value not null fun <T, V, R> LiveData<T>.combineWithNotNull( liveData: LiveData<V>, block: (T, V) -> R ): LiveData<R> { val result = MediatorLiveData<R>() result.addSource(this) { this.value?.let { first -> liveData.value?.let { second -> result.value = block(first, second) } } } result.addSource(liveData) { this.value?.let { first -> liveData.value?.let { second -> result.value = block(first, second) } } } return result }
If you want to create a field and setup at construction time (use also): val liveData1 = MutableLiveData(false) val liveData2 = MutableLiveData(false) // Return true if liveData1 && liveData2 are true val liveDataCombined = MediatorLiveData<Boolean>().also { // Initial value it.value = false // Observing changes it.addSource(liveData1) { newValue -> it.value = newValue && liveData2.value!! } it.addSource(selectedAddOn) { newValue -> it.value = liveData1.value!! && newValue } }
Solved with LiveData extensions fun <T, R> LiveData<T>.map(action: (t: T) -> R): LiveData<R> = Transformations.map(this, action) fun <T1, T2, R> LiveData<T1>.combine( liveData: LiveData<T2>, action: (t1: T1?, t2: T2?) -> R ): LiveData<R> = MediatorLiveData<Pair<T1?, T2?>>().also { med -> med.addSource(this) { med.value = it to med.value?.second } med.addSource(liveData) { med.value = med.value?.first to it } }.map { action(it.first, it.second) }
Java version, if anyone else is stuck working on some old project var fullNameLiveData = LiveDataCombiner.combine( nameLiveData, surnameLiveData, (name, surname) -> name + surname ) public class LiveDataCombiner<First, Second, Combined> { private First first; private Second second; private final MediatorLiveData<Combined> combined = new MediatorLiveData<>(); private final BiFunction<First, Second, Combined> combine; public LiveData<Combined> getCombined() { return combined; } public static <First, Second, Combined>LiveDataCombiner<First, Second, Combined> combine( LiveData<First> firstData, LiveData<Second> secondData, BiFunction<First, Second, Combined> combine ) { return new LiveDataCombiner<>(firstData, secondData, combine); } private LiveDataCombiner( LiveData<First> firstData, LiveData<Second> secondData, BiFunction<First, Second, Combined> combine ) { this.combine = combine; addSource(firstData, value -> first = value); addSource(secondData, value -> second = value); } private <T> void addSource(LiveData<T> source, Consumer<T> setValue) { combined.addSource(source, second -> { setValue.accept(second); emit(combine()); }); } private Combined combine() { return combine.apply(first, second); } private void emit(Combined value) { if (combined.getValue() != value) combined.setValue(value); } }