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

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

Типичное приложение для Android должно отображать список элементов и позволять пользователю выполнять поиск в этом списке. И это то, чего я хочу достичь с помощью своего приложения. Итак, я выполнил реализацию, прочитав множество документации, руководств и даже ответов на stackoverflow. Но я не уверен, правильно ли я делаю это или как я должен это делать. Итак, ниже я показал свой способ реализации библиотеки подкачки с помощью 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 ‹PagedList‹ Event >> либо всех событий, либо отфильтрованных событий в соответствии с поиском. текст. Я действительно борюсь с мыслью, что каждый раз, когда происходит изменение в 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;
}

person Ahad    schedule 10.01.2020    source источник


Ответы (2)


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

  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 класс, как показано ниже, и сделал из filterFoodName измененные liveata.
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. Дополнительную информацию можно найти здесь.

person Mir Milad Hosseiny    schedule 14.01.2020
comment
это отличный ответ. Я буду извлекать уроки из этого. Дай мне это проверить. - person Ahad; 14.01.2020
comment
Если это помогло и это ответ на ваш вопрос, отметьте его как ответ. - person Mir Milad Hosseiny; 19.01.2020
comment
Не могли бы вы объяснить, как разоблаченные живые данные ускоряют выступления? - person Ahad; 20.01.2020
comment
Я думаю, что ваш ответ более полезен и ориентирован на производительность. Ваш ответ должен быть принят. Однако, пожалуйста, объясните, как раскрытые данные в реальном времени помогают производительности. - person Ahad; 20.01.2020
comment
Предположим, пользователи хотят искать слово abc, без защиты от ошибок каждый раз, когда пользователь вводит новый символ, foodDao.loadAllFoodFromSearch будет вызывать. В этом случае он будет звонить 3 раза. Но когда у вас есть дебаунсер, он некоторое время ждет нового обновления (в моем ответе это 400 миллисекунд), и если будет какое-либо обновление, он будет ждать еще и затем вызовет метод foodDao.loadAllFoodFromSearch. Поэтому, если пользователь вводит ключевое слово для поиска достаточно быстро, он вызовет один раз. - person Mir Milad Hosseiny; 20.01.2020

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

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

 @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

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

person George Machibya    schedule 13.01.2020
comment
Я очень ценю ваши старания, но ищу решение комнаты и ее перспективу. - person Ahad; 13.01.2020
comment
Привет @ahad Хорошо, я сделал рабочий учебник специально для вашего запроса, пожалуйста, найдите ссылку ниже на репозиторий github.com/muchbeer/PagingSearchFood - person George Machibya; 13.01.2020
comment
Я очень ценю ваши усилия. Не могли бы вы добавить некоторые подробности здесь, в свой ответ, чтобы я мог узнать больше и с хорошим объяснением, ваш ответ может быть принят. Спасибо. - person Ahad; 14.01.2020
comment
Готово, пожалуйста, проверьте - person George Machibya; 14.01.2020