Реализация поиска с использованием RxJava

Original author: Amit Shekhar
  • Translation
  • Tutorial
В данной статье будет рассмотрена оптимальная и компактная реализация поиска с использованием RxJava для Android, отсеивающая ненужные результаты и уменьшающая количество бесполезных сетевых вызовов.

Пример поиска
Оригинал написан 16 октября 2017. Перевод вольный.

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

Рассмотрим как реализовать поиск лучшим способом с использованием RxJava. Не забывайте, в RxJava есть операторы для всего.

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

Взгляните на элементы RxJava, которые мы будем использовать для реализации поиска:

  • Publish Subject: Если вы не сталкивались с этим оператором, то взгляните на эту статью
  • Filter
  • Debounce
  • DistinctUntilChanged
  • SwitchMap

Начнем


Сначала нужно сделать SearchView observable. Сделаем его с использованием PublishSubject. Я использую SearchView из Android. View может быть любым, с функционалом как у EditText. Для реализации observable нужно реализовать listener для изменения текста в поле.

public class RxSearchObservable {

    public static Observable<String> fromView(SearchView searchView) {

        final PublishSubject<String> subject = PublishSubject.create();

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override
            public boolean onQueryTextSubmit(String s) {
                subject.onComplete();
                return true;
            }

            @Override
            public boolean onQueryTextChange(String text) {
                subject.onNext(text);
                return true;
            }
        });

        return subject;
    }
Замечание от ConstOrVar:
Observable для SearchView будет не совсем корректно работать. Вы на него подписываетесь в onCreate(), но при срабатывании onQueryTextSubmit() произойдет отписка, так как вызовется onComplete. Получается, что повторный поиск не будет работать. Чтобы повторный поиск работал, нужно избавиться от subject.onComplete();
Замечание от Scrobot и BFS:
Не стоит использовать Subject, он существует только для 1 цели: соединять императивный стиль с реактивным. Лучше использовать Observable.create(). Поиск это ровно тот кейс, когда нужно думать о Backpressure, а поскольку практически все сегодня используют RxJava2, то там эта проблема решена с помощью Flowable, и лучше этот кейс зарефакторить на него.
Далее необходимо вызвать созданный метод и добавить вызовы операторов, как в примере ниже.

RxSearchObservable.fromView(searchView)
                .debounce(300, TimeUnit.MILLISECONDS)
                .filter(new Predicate<String>() {
                    @Override
                    public boolean test(String text) throws Exception {
                        if (text.isEmpty()) {
                            return false;
                        } else {
                            return true;
                        }
                    }
                })
                .distinctUntilChanged()
                .switchMap(new Function<String, ObservableSource<String>>() {
                    @Override
                    public ObservableSource<String> apply(String query) throws Exception {
                        return dataFromNetwork(query);
                    }
                })
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(String result) throws Exception {
                        textViewResult.setText(result);
                    }
                });

Теперь обсудим почему используются именно эти операторы и как они работают вместе.

Debounce


Этому оператору передаются параметры времени. В данном случае он отлавливает события ввода пользователем текста, например, «a», «ab», «abc». Ввод часто происходит очень быстро и это чревато большим количеством сетевых вызовов. Но пользователь обычно заинтересован только в результатах для «abc». Итак, нужно отбросить выполнение запросов для «a», «ab». Оператор debounce спешит на помощь. Он ожидает бездействия пользователя в течение переданного в параметре времени. Если во время ожидания будет осуществлен какой-либо ввод, то счётчик ожидания сбросится, отсчет начнется заново, предыдущий переданный результат, например, «a» будет отброшен. Таким образом, оператор debounce передает дальше по цепочке только те элементы, которые продержались без вызова новых событий в течение указанного времени ожидания.
debounce example image

Filter


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

DistinctUntilChanged


Данный оператор используется для того, чтобы избежать дублирования сетевых вызовов. Например, последний поисковой запрос был «abc», затем пользователь удалил «c» и заново ввел «c». Результат снова «abc». Если сетевой вызов уже в процессе с тем же параметром, то оператор distinctUntilChanged не даст осуществить аналогичный вызов повторно. Таким образом, оператор distinctUntilChanged отсеивает повторяющиеся последовательно переданные ему элементы.
distinctUntilChanged example image

SwitchMap


В данном примере этот оператор используется для исключения сетевых вызовов, результаты которых больше не нужно показывать пользователю. Например, последним поисковым запросом был «ab» и есть работающий сетевой вызов для этого запроса, но пользователь в это время вводит «abc». Результат для «ab» больше не нужен, нужен только результат для «abc». SwitchMap спешит на помощь. Он передает дальше результаты только последнего запроса, игнорируя остальные.

Javadoc описывает switchMap следующим образом:

Возвращает новый Observable, применяя переданную функцию для каждого полученного элемента Observable, но передавая далее элементы, созданные только последним полученным Observable.

Мы сделали это! Только представьте насколько было бы сложно реализовать поиск без RxJava.

Если вы хотите рассмотреть полный пример, то взгляните на следующий проект.
Share post

Comments 13

    +2
    Спасибо за статью. В Вашем примере Observable для SearchView будет не совсем корректно работать. Вы на него подписываетесь в onCreate(), но при срабатывании onQueryTextSubmit у Вас произойдет отписка, так как вызовется onComplete. Получается, что повторный поиск не будет работать. Чтобы повторный поиск работал, нужно избавиться от subject.onComplete();
      0
      Спасибо за замечание. Согласен с этим, добавил информацию в статью.
      +1
      В целом, использование subject может вести к неочевидным проблемам. Для создания я бы порекомендовал росмотреть на Observable.create, тем более, что там есть emitter, которому можно установить действие при завершении (полезно чтобы снять TextWatcher) например. Здесь, впрочем, появится нюанс: нужно будет правильно переключить потоки, чтобы тело create выполнилось на главном потоке.
        0
        Это перевод статьи, поэтому правки я не вносил в содержание. Но хотелось бы понять о каких конкретно возможных неочевидных проблемах идет речь?
        +1
        Присоеднюсь к BFS, не стоит использовать Subject, он существует только для 1 цели: соединять императивный стиль с реактивным. По сути, он только для легаси подходит. Поэтому, да, лучше Observable.create(). Для подробностей посмотрите доклад Матвея Малькова — Art Of Rx, учитывая что вы андроид разработчик, вам это будет полезно)
        И второй момент, поиск это ровно тот кейс, когда нужно думать о Backpressure, а поскольку практически все сегодня используют Rx2, то там эта проблема решена с помощью Flowable, и лучше этот кейс зарефакторить на него.
          0
          Спасибо за рекомендацию доклада. Добавлю ваше замечание к статье.
          0
          Спасибо, отличная статья, а еще лучше комментарии ).
            0
            Не являюсь android-разработчиком, но по-моему из-за subscribeOn(Schedulers.io()) установка listener-а на поле ввода будет происходить не в главном потоке
              0
              Насколько я знаю, subscribeOn() указывает в какой поток наблюдаемый источник (в нашем случае subject внутри listener поля ввода) будет передавать создаваемые observable элементы. То есть событие происходит в UI потоке, затем его отлавливает listener в этом же потоке, далее subject внутри listener создаёт событие (onNext()), а возвращаемый observable элемент уже попадает в поток IO.
                0
                Мне почему-то кажется что прав kpcb.
                subscribeOn указывает поток в котором будет происходить подписка. И во время подписки вы создаете слушатель.
                Слушатель в UI потоке будет дергать subject.onNext(). и возвращаемый observable будет тоже получать событие в UI потоке.
                Но дальше у вас идет метод debounce. Это особенный метод, который возвращает observable, события которого будут производиться в computation sheduler. (Scheduler можно изменить — посмотрите перегрузку метода debounce).
                  0
                  Проверил. Subject.onNext() отрабатывает в UI потоке, а Debounce() уже в Computation. Спасибо за внимательность.
              0

              Скажите а вам не надоело пользоваться анонимными классами? На дворе вроде конец 2017 и даже андроид уже более или менее из коробки поддерживает ламбда функции.

                0
                Я бы тоже воспользовался лямбдами и/или ссылками на методы, а использование if для возврата булево значения — бредовая идея. Но это перевод и поэтому я не стал лезть в пример кода автора оригинала.

              Only users with full accounts can post comments. Log in, please.