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