Лучшая практика: фильтры времени выполнения с Room и LiveData
Я работаю на экране, который показывает содержимое комнаты, завернутой в БД, используя утилизацию. Адаптер получает LiveData от ViewModel, которая скрывает вызов запроса для объекта Room DAO. Таким образом, объект LiveData на самом деле является объектом ComputableLiveData, который знает об изменениях в БД комнаты.
Теперь я хочу добавить параметры фильтра на экран. Где / как мне реализовать это в этой настройке Room-LiveData-ViewModel?
Должен ли адаптер или ViewModel "постфильтровать" результаты в LiveData? Должен ли я запрашивать данные из комнаты для каждого изменения фильтра? Могу ли я использовать для этого базовые (вычислимые)LiveData? Если нет, должен ли я действительно создавать новые LiveData для каждого изменения фильтра?
Подобный вопрос обсуждается здесь: перезагрузите RecyclerView после изменения данных с помощью Room, ViewModel и LiveData.
4 ответа
Итак, я закончил делать это так:
- Фрагмент направляет состояние фильтра в ViewModel. Побочный эффект: состояние фильтра может использоваться несколькими (т.е. последующими из-за изменения конфигурации) экземплярами фрагмента. Может быть, вы хотите этого, а может и нет. Я делаю.
- ViewModel содержит экземпляр MediatorLiveData. Он имеет единственный источник: объект Room DB LiveData. Источник просто ищет изменения для посредника. Если фильтр изменяется по фрагменту, источник заменяется запросом.
Отвечая на мои подробные вопросы:
- Нет постфильтрации
- Да, запрос на замену фильтра
- Я не использую ComputableLiveData (не уверен, что это будет возможно)
По поводу обсуждения в комментариях:
- Я не применяю пейджинг
Последнее замечание о комнате: я не прав или мне нужно написать отдельные методы DAO для каждой комбинации фильтров, которую я хочу применить? Хорошо, я мог бы вставить необязательные части оператора select через строку, но тогда я бы потерял преимущества Room. Был бы неплох какой-то конструктор операторов, который делает составные операторы.
Спасибо CommonsWare и pskink за вашу помощь!
Я работаю в аналогичной проблеме. Изначально у меня был RxJava, но теперь я конвертирую его в LiveData.
Вот как я делаю внутри моей ViewModel:
// Inside ViewModel
MutableLiveData<FilterState> modelFilter = new MutableLiveData<>();
LiveData<PagedList<Model>> modelLiveData;
Эта modelLivedata создается следующим образом в конструкторе модели представления:
// In ViewModel constructor
modelLiveData = Transformations.switchMap(modelFilter,
new android.arch.core.util.Function<FilterState, LiveData<PagedList<Model>>>() {
@Override
public LiveData<PagedList<Model>> apply(FilterState filterState) {
return modelRepository.getModelLiveData(getQueryFromFilter(filterState));
}
});
Когда модель представления получает другой фильтр для применения, она делает:
// In ViewModel. This method receives the filtering data and sets the modelFilter
// mutablelivedata with this new filter. This will be "transformed" in new modelLiveData value.
public void filterModel(FilterState filterState) {
modelFilter.postValue(filterState);
}
Затем этот новый фильтр будет "преобразован" в новое значение жизненных данных, которое будет отправлено наблюдателю (фрагмент).
Фрагмент получает живые данные для наблюдения посредством вызова в модели представления:
// In ViewModel
public LiveData<PagedList<Model>> getModelLiveData() {
return modelLiveData;
}
И внутри моего фрагмента у меня есть:
@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
ViewModel viewModel = ViewModelProviders.of(this.getActivity()).get(ViewModel.class);
viewModel.getModelLiveData().observe(this.getViewLifecycleOwner(), new Observer<PagedList<Model>>() {
@Override
public void onChanged(@Nullable PagedList<Model> model) {
modelListViewAdapter.submitList(model);
}
});
}
Я надеюсь, что это помогает.
Основываясь на ответе Франциско (большое вам спасибо за это!), Вот как я реализовал аналогичную динамическую фильтрацию базы данных на основе ввода EditText, но в Kotlin.
Вот пример запроса Dao, где я выполняю выбор на основе переданной строки фильтра:
// Dao query with filter
@Query("SELECT * from myitem WHERE name LIKE :filter ORDER BY _id")
fun getItemsFiltered(filter: String): LiveData<List<MyItem>>
У меня есть репозиторий, но в данном случае это просто сквозной проход. Если у вас нет репозитория, вы можете вызвать метод dao прямо из ViewModel.
// Repository
fun getItemsFiltered(filter: String): LiveData<List<MyItem>> {
return dao.getItemsFiltered(filter)
}
Затем в ViewModel я использую метод Transformations, который также использовал Франциско. Однако мой фильтр - это всего лишь простая строка, завернутая в MutableLiveData. Метод setFilter публикует новое значение фильтра, которое, в свою очередь, вызывает преобразование allItemsFiltered.
// ViewModel
var allItemsFiltered: LiveData<List<MyItem>>
var filter = MutableLiveData<String>("%")
init {
allItemsFiltered = Transformations.switchMap(filter) { filter ->
repository.getItemsFiltered(filter)
}
}
// set the filter for allItemsFiltered
fun setFilter(newFilter: String) {
// optional: add wildcards to the filter
val f = when {
newFilter.isEmpty() -> "%"
else -> "%$newFilter%"
}
filter.postValue(f) // apply the filter
}
Обратите внимание, что начальное значение фильтра установлено на подстановочный знак ("%"), чтобы по умолчанию возвращались все элементы. Если вы не установите это значение, никакие элементы не будут наблюдаться, пока вы не вызовете setFilter.
Вот код во фрагменте, где я наблюдаю allItemsFiltered, а также применяю фильтрацию. Обратите внимание, что я обновляю фильтр, когда мой поисковый EditText изменяется, а также при восстановлении состояния просмотра. Последний установит ваш начальный фильтр, а также восстановит существующее значение фильтра при повороте экрана (если ваше приложение поддерживает это).
// Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// observe the filtered items
viewModel.allItemsFiltered.observe(viewLifecycleOwner, Observer { items ->
// update the displayed items when the filtered results change
items.let { adapter.setItems(it) }
})
// update the filter as search EditText input is changed
search_et.addTextChangedListener {text: Editable? ->
if (text != null) viewModel.setFilter(text.toString())
}
}
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
// update the filter to current search text (this also restores the filter after screen rotation)
val filter = search_et.text?.toString() ?: ""
viewModel.setFilter(filter)
}
Надеюсь, это поможет!!
Отказ от ответственности: это мой первый пост, поэтому дайте мне знать, если я что-то пропустил. Я не уверен, как связать с ответом Франциско, иначе я бы это сделал. Это определенно помогло мне добраться до моей реализации.
Вы можете отсортировать базу данных, используя
CASE WHEN
а также
THEN
Посмотрите на этот код
Создайте постоянный класс для сортировки идентификатора
object Constant{
const val NAME_ASC = 1
const val NAME_DESC = 2
const val ADDED_ASC = 3
const val ADDED_DESC = 4
}
Интерфейс Дао
@Query(
"SELECT * FROM table WHERE name=:name ORDER BY" +
" CASE WHEN :sortBy = $NAME_ASC THEN title END ASC , " +
" CASE WHEN :sortBy = $NAME_DESC THEN title END DESC , " +
" CASE WHEN :sortBy = $ADDED_ASC THEN added END ASC , " +
" CASE WHEN :sortBy = $ADDED_DESC THEN added END DESC , " +
)
fun getItems(name: String, sortBy: Int): MutableLiveData<Item>
Ваш репозиторий Класс
fun getItems(name: String, sortBy: Int) : MutableLiveData<Items>{
return myDao.getItems(name,sortBy)
}