MediatorLiveData or switchMap transformation with multiple parameters

23,817

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.

Share:
23,817
Yann39
Author by

Yann39

Computer scientist engineer from Switzerland. Master degree in Databases and Artificial Intelligence. Passionate about new technologies, love to get into everything. Open source promoter. Love Java, mobile apps, Blockchain and UI stuff.

Updated on July 19, 2022

Comments

  • Yann39
    Yann39 almost 2 years

    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.

  • amlwin
    amlwin almost 6 years
    How can i make a network call which require 4 LiveData (eg. userId, emial ,apiKey and so on ..).Transformation.switchMap accept only one LiveData paramPlease give some suggestion. Thz
  • Akshay Chordiya
    Akshay Chordiya almost 6 years
    I came across a similar situation and I used MediatorLiveData. I'd recommend about it and see if it fits your use-case else you could use RxJava and then convert it into LiveData.
  • amlwin
    amlwin almost 6 years
    if you wouldn't mind, could you share me some code snippet?
  • Richard Le Mesurier
    Richard Le Mesurier over 5 years
    dead link unfortunately
  • Akshay Chordiya
    Akshay Chordiya over 5 years
    @RichardLeMesurier It keeps becoming invalid because there are constant changes in the repository. Hence now I have added a link to my gist so it doesn't break
  • Richard Le Mesurier
    Richard Le Mesurier over 5 years
    Frustrating, yes. I also just posted an image link to an answer and I know that soon it too will become dead...
  • aleksandrbel
    aleksandrbel about 5 years
    Thanks, very helpful! But in this case switchMap is init two times in a row, is it possible to skip 1st source in this case?
  • jL4
    jL4 almost 5 years
    @aleksandrbel Do you mean that switchMap can be triggered twice using separate setters for code and nbDays? Then yeah, that can cause the observer to receive two back-to-back values. If you don't want that behaviour, it would be better to use only a single Pair object in the MutableLiveData that combines both values like in the answer by OP below.
  • james04
    james04 almost 4 years
    Thats a very nice solution. Is there anyway to extend it with N sources?? I have a case that the sources number is unknown..!
  • aeracode
    aeracode over 3 years
    You may want to implement "equals" in DayPriceFilter.