Searching a LiveData of PagedList in RecyclerView by Observing ViewModel

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.


I will strong advice to start using RxJava and you it can simplify the entire problem of looking on the search logic.

I recommend in the Dao Room Class you implement two method, one to query all the data when the search is empty and the other one is to query for the searched item as follows. Datasource is used to load data in the pagelist

 @Query("SELECT * FROM food order by food_name")
 DataSource.Factory<Integer, Food> loadAllFood();

@Query("SELECT * FROM food where food_name LIKE  :name order by food_name")
DataSource.Factory<Integer, Food> loadAllFoodFromSearch(String name);

In the ViewModel Class we need to two parameter that one will be used to observed searched text and that we use MutableLiveData that will notify the Views during OnChange. And then LiveData to observe the list of Items and update the UI. SwitchMap apply the function that accept the input LiveData and generate the corresponding LiveData output. Please find the below Code

public LiveData<PagedList<Food>> listAllFood;
public MutableLiveData<String> filterFoodName = new MutableLiveData<>();

public void initialFood(final FoodDao foodDao) {
    this.foodDao = foodDao;

    PagedList.Config config = (new PagedList.Config.Builder())
            .setPageSize(10)
            .build();

    listAllFood = Transformations.switchMap(filterFoodName, outputLive -> {

               if (outputLive == null || outputLive.equals("") || input.equals("%%")) {
                //check if the current value is empty load all data else search
                return new LivePagedListBuilder<>(
                        foodDao.loadAllFood(), config)
                        .build();
            } else {
                   return new LivePagedListBuilder<>(
                        foodDao.loadAllFoodFromSearch(input),config)
                        .build();
            }
        });
    }

The viewModel will then propagate the LiveData to the Views and observe the data onchange. In the MainActivity then we call the method initialFood that will utilize our SwitchMap function.

  viewModel = ViewModelProviders.of(this).get(FoodViewModel.class);
  viewModel.initialFood(FoodDatabase.getINSTANCE(this).foodDao());

  viewModel.listAllFood.observe(this, foodlistPaging -> {
        try {
     Log.d(LOG_TAG, "list of all page number " + foodlistPaging.size());

            foodsactivity = foodlistPaging;
            adapter.submitList(foodlistPaging);

        } catch (Exception e) {
        }
    });

  recyclerView.setAdapter(adapter);

For the first onCreate initiate filterFoodName as Null so that to retrieve all items. viewModel.filterFoodName.setValue("");

Then apply TextChangeListener to the EditText and call the MutableLiveData that will observe the Change and update the UI with the searched Item.

searchFood.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence charSequence, int i, 
int i1, int i2) {

            }

            @Override
            public void onTextChanged(CharSequence charSequence, int i, int 
 i1, int i2) {

            }

            @Override
            public void afterTextChanged(Editable editable) {
                //just set the current value to search.
                viewModel.filterFoodName.
                        setValue("%" + editable.toString() + "%");
            }
        });
    }

Below is my github repo of full code.

https://github.com/muchbeer/PagingSearchFood

Hope that help