Search code examples
androidandroid-recyclerviewsearchviewandroid-viewmodelandroid-paging

Searching a LiveData of PagedList in RecyclerView by Observing ViewModel


With android Paging library it is really easy to load data from Database in chunks and ViewModel provides automatic UI update and data survival. All these frameworks modules help us create a great app in android platform.

A typical android app has to show a list of items and allows user to search that list. And this what I want to achieve with my app. So I have done an implementation by reading many documentations, tutorials and even stackoverflow answers. But I am not so sure whether I am doing it correctly or how I supposed to do it. So below, I have shown my way of implementing paging library with ViewModel and RecyclerView.

Please, review my implementation and correct me where I am wrong or show me how I supposed to do it. I think there are many new android developers like me are still confused how to do it correctly as there is no single source to have answers to all your questions on such implementation.

I am only showing what I think is important to show. I am using Room. Here is my Entity that I am working with.

@Entity(tableName = "event")
public class Event {
    @PrimaryKey(autoGenerate = true)
    public int id;

    public String title;
}

Here is DAO for Event entity.

@Dao
public interface EventDao {
    @Query("SELECT * FROM event WHERE event.title LIKE :searchTerm")
    DataSource.Factory<Integer, Event> getFilteredEvent(String searchTerm);
}

Here is ViewModel extends AndroidViewModel which allows reading and searching by providing LiveData< PagedList< Event>> of either all events or filtered event according to search text. I am really struggling with the idea that every time when there is a change in filterEvent, I'm creating new LiveData which can be redundant or bad.

private MutableLiveData<Event> filterEvent = new MutableLiveData<>();
private LiveData<PagedList<Event>> data;

private MeDB meDB;

public EventViewModel(Application application) {
    super(application);
    meDB = MeDB.getInstance(application);

    data = Transformations.switchMap(filterEvent, new Function<Event, LiveData<PagedList<Event>>>() {
        @Override
        public LiveData<PagedList<Event>> apply(Event event) {
            if (event == null) {
                // get all the events
                return new LivePagedListBuilder<>(meDB.getEventDao().getAllEvent(), 5).build();
            } else {
                // get events that match the title
                return new LivePagedListBuilder<>(meDB.getEventDao()
                          .getFilteredEvent("%" + event.title + "%"), 5).build();
            }
        }
    });
}

public LiveData<PagedList<Event>> getEvent(Event event) {
    filterEvent.setValue(event);
    return data;
}

For searching event, I am using SearchView. In onQueryTextChange, I wrote the following code to search or to show all the events when no search terms is supplied meaning searching is done or canceled.

Event dumpEvent;

@Override
public boolean onQueryTextChange(String newText) {

    if (newText.equals("") || newText.length() == 0) {
        // show all the events
        viewModel.getEvent(null).observe(this, events -> adapter.submitList(events));
    }

    // don't create more than one object of event; reuse it every time this methods gets called
    if (dumpEvent == null) {
        dumpEvent = new Event(newText, "", -1, -1);
    }

    dumpEvent.title = newText;

    // get event that match search terms
    viewModel.getEvent(dumpEvent).observe(this, events -> adapter.submitList(events));

    return true;
}

Solution

  • Thanks to George Machibya for his great answer. But I prefer to do some modifications on it as bellow:

    1. There is a trade off between keeping none filtered data in memory to make it faster or load them every time to optimize memory. I prefer to keep them in memory, so I changed part of code as bellow:
    listAllFood = Transformations.switchMap(filterFoodName), input -> {
                if (input == null || input.equals("") || input.equals("%%")) {
                    //check if the current value is empty load all data else search
                    synchronized (this) {
                        //check data is loaded before or not
                        if (listAllFoodsInDb == null)
                            listAllFoodsInDb = new LivePagedListBuilder<>(
                                    foodDao.loadAllFood(), config)
                                    .build();
                    }
                    return listAllFoodsInDb;
                } else {
                    return new LivePagedListBuilder<>(
                            foodDao.loadAllFoodFromSearch("%" + input + "%"), config)
                            .build();
                }
            });
    
    1. Having a debouncer helps to reduce number of queries to database and improves performance. So I developed DebouncedLiveData class as bellow and make a debounced livedata from filterFoodName.
    public class DebouncedLiveData<T> extends MediatorLiveData<T> {
    
        private LiveData<T> mSource;
        private int mDuration;
        private Runnable debounceRunnable = new Runnable() {
            @Override
            public void run() {
                DebouncedLiveData.this.postValue(mSource.getValue());
            }
        };
        private Handler handler = new Handler();
    
        public DebouncedLiveData(LiveData<T> source, int duration) {
            this.mSource = source;
            this.mDuration = duration;
    
            this.addSource(mSource, new Observer<T>() {
                @Override
                public void onChanged(T t) {
                    handler.removeCallbacks(debounceRunnable);
                    handler.postDelayed(debounceRunnable, mDuration);
                }
            });
        }
    }
    

    And then used it like bellow:

    listAllFood = Transformations.switchMap(new DebouncedLiveData<>(filterFoodName, 400), input -> {
    ...
    });
    
    1. I usually prefer to use DataBiding in android. By using two way Data Binding you don't need to use TextWatcher any more and you can bind your TextView to the viewModel directly.

    BTW, I modified George Machibya solution and pushed it in my Github. For more details you can see it here.