LiveData with filters?
Asked Answered
B

2

8

I'm new to LiveData and I've been doing some tests lately. I have an app where I need to display data that can be filtered (name, category, date...). The filters can be combined too (name + date). This data comes from an API call with Retrofit + RXJava.

I know that I can have the data directly on my view without using LiveData. However, I thought that it would be interesting to use a ViewModel + LiveData. First, to test how it works but also to avoid trying to set data if the view is not active (thanks to LiveData) and save the data in case of configuration changes (thanks to ViewModel). These were things that I had to handle manually before.

So the problem is that I didn't find a way to easily handle the filters with LiveData. In cases where the user chooses one filter I managed to make it work with switchMap:

return Transformations.switchMap(filter,
    filter -> LiveDataReactiveStreams.fromPublisher(
    repository.getData(filter).toFlowable(BackpressureStrategy.BUFFER)));

If he chooses two filters, I saw that I could use a custom MediatorLiveData and that's what I did. However, the problem here is that my repository call is done as many times as the number of filters I have and I can't set two filters at the same time.

My custom MediatorLiveData:

class CustomLiveData extends MediatorLiveData<Filter> {

    CustomLiveData(LiveData<String> name, LiveData<String> category) {
        addSource(name, name -> {
            setValue(new Filter(name, category.getValue()));
        });

        addSource(category, category -> {
            setValue(new Filter(name.getValue(), newCategory));
        });
    }
}
CustomLiveData trigger = new CustomLiveData(name, category);

  return Transformations.switchMap(trigger,
     filter -> LiveDataReactiveStreams.fromPublisher(
        repository.getData(filter.getName(), filter.getCategory())
        .toFlowable(BackpressureStrategy.BUFFER)));

Did I understand well the usage of MediatorLiveData? Is it possible to do what I'm trying to achieve with LiveData?

Thanks!

Bagehot answered 9/5, 2020 at 8:57 Comment(0)
A
7

The way you set this up in your answer, every time you call updateData() you will be adding a new source to your MediatorLiveData. Do you need the different sources every time a new name and category is changed?

Also, could you have your repository method getData expose a LiveData directly? And finally, would it be possible to use Kotlin instead of Java?

If so, you can simplify your flow by checking conditionally on the onChanged of each source when to set the value in your data MediatorLiveData.

For example, following the documentation here:

//Create a new instance of MediatorLiveData if you don't need to maintain the old sources
val data = MediatorLiveData<List<Data>>().apply {
    addSource(repository.getData(name, category)) { value ->
        if (yourFilterHere) {
            this.value = value
        }
    }
}

You could also extend this idea to create an extension function with Kotlin:

inline fun <T> LiveData<T>.filter(crossinline filter: (T?) -> Boolean): LiveData<T> {
        return MediatorLiveData<T>().apply {
            addSource(this@filter) {
                if (filter(it)) {
                    this.value = it
                }
            }
        }
    }

Then, in your updateData() method you can do:

val data = repository.getData(name, category)
    .filter { value ->
        // Your boolean expression to validate the values to return
    }
Aarika answered 14/5, 2020 at 20:24 Comment(2)
Here are my answers to your questions: Do you need the different sources every time a new name and category is changed? No, I need one at a time. Not the two together. Also, could you have your repository method getData expose a LiveData directly? Yes and this was pointed me in the right direction. Thanks for that! And finally, would it be possible to use Kotlin instead of Java? No :/ I couldn't make it work the way you suggested but your comment is what helped me. //Create a new instance of MediatorLiveData What I did is to remove the sources before adding a new one.Bagehot
Unless I am missing something the extension does not work : in the apply block this refer to the MediatorLiveData, i.e. it adds itself as source (and the original LiveData is not used at all). I think it can easily fixed by using also insteadTemperature
B
0

I think I was seeing the problem from a wrong angle. Correct me if I'm wrong, please.

I was trying to update the LiveData based on the changes of other LiveDatas when I could update my LiveData directly. The solution I found is to have a MediatorLiveData that is updated directly by the view. It could be a MutableLiveData but as I'm using LiveDataReactiveStreams and it doesn't accept a MutableLiveData I didn't find another solution.

public class MainViewModel extends AndroidViewModel {

    // Had to use a MediatorLiveData because the LiveDataReactiveStreams does not accept a MutableLiveData
    private MediatorLiveData<List<Data>> data = new MediatorLiveData<>();

    public MainViewModel(@NonNull Application application) {
        super(application);

        data.addSource(
            LiveDataReactiveStreams.fromPublisher(
                repository.getData(name, category)
                    .toFlowable(BackpressureStrategy.BUFFER)
            ), value -> data.setValue(value));
    }

    public LiveData<List<Data>> getData() {
        return data;
    }

    public void updateData(String name, String category) {
        data.addSource(
            LiveDataReactiveStreams.fromPublisher(
                repository.getData(name, category)
                    .toFlowable(BackpressureStrategy.BUFFER)
            ), value -> data.setValue(value));
    }
}

And then on the activity I simply call it like this:

viewModel.updateData(name, category);

I don't know if I should remove a source before trying to add another but my API calls are only done once with this solution and I can have both filters.

Bagehot answered 9/5, 2020 at 11:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.