11

I have two DAOs, two Repositories and two POJOs. There is some way to create one Livedata of two? I need it to make single list for Recyclerview. POJOs are similar objects.

ExpenseRepository:

public class ExpenseRepository {

private ExpenseDao expenseDao;
private LiveData<List<Expense>> allExpenses;

public ExpenseRepository(Application application) {
    ExpenseIncomeDatabase database = ExpenseIncomeDatabase.getInstance(application);
    expenseDao = database.expenseDao();
    allExpenses = expenseDao.getExpensesByDay();
}

public LiveData<List<Expense>> getAllExpensesByDay() {
    return allExpenses;
}

IncomeRepository:

public class IncomeRepository {

private IncomeDao incomeDao;
private LiveData<List<Income>> allIncomes;

public IncomeRepository(Application application) {
    ExpenseIncomeDatabase database = ExpenseIncomeDatabase.getInstance(application);
    incomeDao = database.incomeDao();
    allIncomes = incomeDao.getIncomesByDay();
}

public LiveData<List<Income>> getAllIncomesByDay() {
    return allIncomes;
}

ExpenseDao:

@Dao
public interface ExpenseDao {

@Query("SELECT * FROM expense_table ORDER BY day") 
LiveData<List<Expense>> getExpensesByDay();

IncomeDao:

@Dao
public interface IncomeDao {

@Query("SELECT * FROM income_table ORDER BY day") 
LiveData<List<Income>> getIncomesByDay();

DailyViewModel:

public class DailyFragmentViewModel extends AndroidViewModel {

private ExpenseRepository expenseRepository;
private IncomeRepository incomeRepository;
private LiveData<Pair<List<Expense>, List<Income>>> combined;
private ExpenseDao expenseDao;
private IncomeDao incomeDao;

public DailyFragmentViewModel(@NonNull Application application) {
    super(application);
    expenseRepository = new ExpenseRepository(application);
    incomeRepository = new IncomeRepository(application);
    combined = new DailyCombinedLiveData(expenseDao.getExpensesByDay(), incomeDao.getIncomesByDay());
}

public LiveData<Pair<List<Expense>, List<Income>>> getExpensesAndIncomes() {
    return combined;
}
beginner992
  • 659
  • 1
  • 9
  • 28
  • 1
    check `MediatorLiveData` – Emil Mar 07 '19 at 12:56
  • 1
    Well seems like a usecase of [`MediatorLiveData`](https://proandroiddev.com/mediatorlivedata-to-the-rescue-5d27645b9bc3) . PS i am not sure cause i never used it . Give it a deep read. – ADM Mar 07 '19 at 13:03

3 Answers3

18

I assume you want to combine them, yes? You'll need a MediatorLiveData, but the guy saying you now need Object is wrong. What you need is a MediatorLiveData<Pair<List<Expense>, List<Income>>>.

public class CombinedLiveData extends MediatorLiveData<Pair<List<Expense>, List<Income>>> {
    private List<Expense> expenses = Collections.emptyList();
    private List<Income> incomes = Collections.emptyList();

    public CombinedLiveData(LiveData<List<Expense>> ld1, LiveData<List<Income>> ld2) {
        setValue(Pair.create(expenses, incomes));

        addSource(ld1, expenses -> { 
             if(expenses != null) {
                this.expenses = expenses;
             } 
             setValue(Pair.create(expenses, incomes)); 
        });

        addSource(ld2, incomes -> { 
            if(incomes != null) {
                this.incomes = incomes;
            } 
            setValue(Pair.create(expenses, incomes));
        });
    }
}

You could potentially make this generic and it'd be the implementation of combineLatest for two LiveData using tuples of 2-arity (Pair).

EDIT: like this:

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));
        });
    }
}

Beware that I lost the ability to set Collections.emptyList() as initial values of A and B with this scenario, and you WILL need to check for nulls when you access the data inside the pair.

EDIT: You can use the library https://github.com/Zhuinden/livedata-combinetuple-kt (Kotlin) or https://github.com/Zhuinden/livedata-combineutil-java/ (Java) which does the same thing.

EpicPandaForce
  • 79,669
  • 27
  • 256
  • 428
  • in this case, if `expenses` livedata is triggered then the `income` data will be also available which might be null or old data. I think implementing a generic class will be a proper solution. – Emil Mar 07 '19 at 13:48
  • It will never be null, but it can be empty list. Whether that is expected or not depends on whether you need zip or combineLatest. You could potentially track "did emit" with booleans and when all of them are true then emit once? – EpicPandaForce Mar 07 '19 at 13:58
  • It looks interesting. I should to create CombinedLiveData class, or make for this common repository? – beginner992 Mar 07 '19 at 15:53
  • Well all I know is that if you receive this mediator instance as a `LiveData, List>>` then you'll receive both arguments, whenever either of them changes. I think in ViewModel you could just create it like `new CombinedLiveData(dao.findExpenses(), dao.findIncomes())` and it would work. – EpicPandaForce Mar 07 '19 at 15:54
  • @EpicPandaForce ya, it won't be null, that was my bad. But as you said we will receive both arguments whenever either of them changes. So I am wondering does generic implementation will resolve this problem? – Emil Mar 07 '19 at 19:52
  • @EpicPandaForce Unfortunately there is something wrong... I have null in: androidx.lifecycle.LiveData com.example.budgettracker.data.db.ExpenseDao.getExpensesByDay() I hope I have done everything right. I just pasted ViewModel above. – beginner992 Mar 07 '19 at 22:10
  • ... I might need to check for nulls in the lambdas given to addSource, I wasn't expecting that :/ – EpicPandaForce Mar 07 '19 at 22:37
  • Maybe I'll try to make a shared repository for Income and Expense? I even found something here: https://stackoverflow.com/questions/53621531/combine-two-livedata-objects-with-the-same-observer-types What you think? – beginner992 Mar 07 '19 at 23:31
  • That answer would only store 1 List, and you wouldn't be able to get both of them at once. Each new update would overwrite the list with the other entity type. – EpicPandaForce Mar 08 '19 at 11:55
  • If this doesn't work even with the null-check, I'll write the generic version for you. – EpicPandaForce Mar 08 '19 at 13:09
  • It would be great. – beginner992 Mar 08 '19 at 16:54
  • i added it. Also I think I noticed what was wrong with the first version, I should have also added a `setValue(` to initialize the mediator in the constructor. Maybe check with that? – EpicPandaForce Mar 08 '19 at 22:13
  • I'm not 100% sure, but it works rather. I also changed in ViewModel constructor in `new DailyCombinedLiveData` from `expenseDao.getExpensesByDay()` to `expenseRepository.getExpenseByDay()` and there is no null. But I have problem with set pair in recycler view adapter. There is some special way? – beginner992 Mar 08 '19 at 23:06
  • Yep, I'm sure, your solution works. I have only problem with set pair to the double view holder recyclerview. If you have some idea how to properly set would be great. Thank you for you big help! – beginner992 Mar 08 '19 at 23:17
  • 1
    Hrmm you *should* be able to use two different ViewTypes as long as you pass both List to the adapter, but *diffing* the lists is complicated. For that, the easy way out is to use a library that can "unify the way you handle multiple view types", such as https://github.com/lisawray/groupie/ . – EpicPandaForce Mar 08 '19 at 23:44
  • @EpicPandaForce, how to handle this use case: assuming the liveData is posting a warp class `{data, state}` so that one liveData for two type posts. But in the flow of os kills activity and restores it, when do the `liveData.observe()` it will immediately get the last post from the liveData. If the last post was `state` type before os kills activity, which was post after some `data` post to the liveData before that, the restored activity could not get the previously posted `data` (but only the `state`), so UI experience couldnt be restored without those data. How to resolve this issue? – lannyf Nov 04 '19 at 13:44
  • @lannyf show a loading view while you only have state but no data. Then when you have the data, you can render it correctly based on your state. That's why preserving selected item IDs for example is quite important. – EpicPandaForce Nov 04 '19 at 13:46
  • @EpicPandaForce thx! but still have problem to combine two types in one liveData, the UI was showing a data list and get an error state, then os kills the activity and restores it. the restoring flow `liveData.observe()` only receives the `error` state. Looks like it has to add different code to handle if it is a fresh `liveData.observe()`, or a `liveData.observe()`, and in the restoring flow in that case it needs to extra data querying to restore the UI (cannt simply get the last post data from the liveData). Isnt it better to use two liveData for its own type? – lannyf Nov 04 '19 at 13:58
  • This answer is for combining two LiveData if they need to notify you if either of the observed underlying LiveData changes. If you have two separate LiveData, then you don't necessarily need to combine them. – EpicPandaForce Nov 04 '19 at 22:20
3

Created this extension function in kotlin

fun <A, B> LiveData<A>.zipWith(stream: LiveData<B>): LiveData<Pair<A, B>> {
 val result = MediatorLiveData<Pair<A, B>>()
  result.addSource(this) { a ->
    if (a != null && stream.value != null) {
        result.value = Pair(a, stream.value!!)
    }
  }
 result.addSource(stream) { b ->
    if (b != null && this.value != null) {
        result.value = Pair(this.value!!, b)
    }
 }
 return result
}
iamanbansal
  • 2,412
  • 2
  • 12
  • 20
1

Instead of having a class to add 2 live datas, another class to add 3 live datas, etc. We can use a more abstract way where we can add as many live datas as we want.

import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

/**
 * 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.
 * @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)
            }
        }
    }
}
Damia Fuentes
  • 5,308
  • 6
  • 33
  • 65