Поиск LiveData PagedList в RecyclerView путем наблюдения за ViewModel

С библиотекой Android Paging действительно легко загружать данные из базы данных по частям, а ViewModel обеспечивает автоматическое обновление пользовательского интерфейса и сохранение данных. Все эти модули фреймворков помогают нам создать отличное приложение на платформе Android.

Типичное приложение для Android должно отображать список элементов и позволять пользователю выполнять поиск в этом списке. И этого я хочу достичь с помощью своего приложения. Итак, я выполнил реализацию, прочитав множество документации, руководств и даже ответов на stackru. Но я не уверен, правильно ли я это делаю и как я должен это делать. Итак, ниже я показал свой способ реализации библиотеки подкачки с помощью ViewModel и RecyclerView.

Пожалуйста, просмотрите мою реализацию и исправьте меня, где я ошибаюсь, или покажите мне, как я должен это делать. Я думаю, что многие новые разработчики Android, такие как я, все еще не понимают, как это сделать правильно, поскольку нет единого источника, который мог бы получить ответы на все ваши вопросы по такой реализации.

Я показываю только то, что считаю важным показать. Я использую Room. Вот моя сущность, с которой я работаю.

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

    public String title;
}

Вот DAO для объекта Event.

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

Вот ViewModel расширяет AndroidViewModel, который позволяет читать и искать, предоставляя LiveData> либо всех событий, либо отфильтрованных событий в соответствии с текстом поиска. Я действительно борюсь с идеей, что каждый раз, когда происходит изменение в filterEvent, я создаю новые LiveData, которые могут быть избыточными или плохими.

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;
}

Для поиска событий я использую SearchView. В onQueryTextChange я написал следующий код для поиска или отображения всех событий, когда условия поиска не указаны, что означает, что поиск выполнен или отменен.

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;
}

2 ответа

Решение

Спасибо George Machibya за отличный ответ. Но я предпочитаю внести в него некоторые изменения, как показано ниже:

  1. Существует компромисс между сохранением не отфильтрованных данных в памяти, чтобы ускорить процесс, или загрузкой их каждый раз для оптимизации памяти. Я предпочитаю хранить их в памяти, поэтому я изменил часть кода, как показано ниже:
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. Наличие средства защиты от ошибок помогает сократить количество запросов к базе данных и повысить производительность. Так я разработалDebouncedLiveData класс, как ниже, и сделать дебаундованный liveata из 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);
            }
        });
    }
}

А затем использовал его, как показано ниже:

listAllFood = Transformations.switchMap(new DebouncedLiveData<>(filterFoodName, 400), input -> {
...
});
  1. Обычно я предпочитаю использовать DataBiding в android. Используя двухстороннюю привязку данных, вам не нужно использоватьTextWatcher больше, и вы можете напрямую привязать свой TextView к viewModel.

Кстати, я модифицировал решение Джорджа Мачибьи и поместил его в свой Github. Более подробную информацию вы можете увидеть здесь.

Я настоятельно рекомендую начать использовать RxJava, и вы можете упростить всю проблему просмотра логики поиска.

Я рекомендую в классе комнаты Dao реализовать два метода: один для запроса всех данных, когда поиск пуст, а другой - для запроса искомого элемента следующим образом. Источник данных используется для загрузки данных в список страниц.

 @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);

В классе ViewModel нам нужно два параметра, один из которых будет использоваться для наблюдаемого искомого текста и что мы используем MutableLiveData, который будет уведомлять представления во время OnChange. А затем LiveData для просмотра списка элементов и обновления пользовательского интерфейса. SwitchMap применяет функцию, которая принимает входные данные LiveData и генерирует соответствующие выходные данные LiveData. Пожалуйста, найдите код ниже

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();
            }
        });
    }

Затем viewModel будет распространять LiveData на представления и наблюдать за изменением данных. Затем в MainActivity мы вызываем метод initialFood, который будет использовать нашу функцию SwitchMap.

  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);

Для первого onCreate инициализируйте filterFoodName как Null, чтобы получить все элементы. viewModel.filterFoodName.setValue("");

Затем примените TextChangeListener к EditText и вызовите MutableLiveData, который будет наблюдать за изменением и обновлять пользовательский интерфейс с помощью найденного элемента.

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() + "%");
            }
        });
    }

Ниже мой репозиторий с полным кодом на github.

https://github.com/muchbeer/PagingSearchFood

Надеюсь, что поможет

Другие вопросы по тегам