MediatorLiveData or switchMap transformation with multiple parameters

Solution 1:

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

Solution 2:

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

Solution 3:

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.