Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
}
like image 491
Ahad Avatar asked Jan 10 '20 16:01

Ahad


2 Answers

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.

like image 192
Mir Milad Hosseiny Avatar answered Nov 15 '22 01:11

Mir Milad Hosseiny


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

like image 35
George Machibya Avatar answered Nov 15 '22 00:11

George Machibya