В данной статье будет рассмотрена оптимальная и компактная реализация поиска с использованием 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.

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

Комментарии (11)


  1. ConstOrVar
    22.12.2017 12:20

    Спасибо за статью. В Вашем примере Observable для SearchView будет не совсем корректно работать. Вы на него подписываетесь в onCreate(), но при срабатывании onQueryTextSubmit у Вас произойдет отписка, так как вызовется onComplete. Получается, что повторный поиск не будет работать. Чтобы повторный поиск работал, нужно избавиться от subject.onComplete();


    1. tehreh1uneh Автор
      22.12.2017 13:14

      Спасибо за замечание. Согласен с этим, добавил информацию в статью.


  1. BFS
    22.12.2017 23:45

    В целом, использование subject может вести к неочевидным проблемам. Для создания я бы порекомендовал росмотреть на Observable.create, тем более, что там есть emitter, которому можно установить действие при завершении (полезно чтобы снять TextWatcher) например. Здесь, впрочем, появится нюанс: нужно будет правильно переключить потоки, чтобы тело create выполнилось на главном потоке.


    1. tehreh1uneh Автор
      23.12.2017 05:21

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


  1. Scrobot
    23.12.2017 06:19

    Присоеднюсь к BFS, не стоит использовать Subject, он существует только для 1 цели: соединять императивный стиль с реактивным. По сути, он только для легаси подходит. Поэтому, да, лучше Observable.create(). Для подробностей посмотрите доклад Матвея Малькова — Art Of Rx, учитывая что вы андроид разработчик, вам это будет полезно)
    И второй момент, поиск это ровно тот кейс, когда нужно думать о Backpressure, а поскольку практически все сегодня используют Rx2, то там эта проблема решена с помощью Flowable, и лучше этот кейс зарефакторить на него.


    1. tehreh1uneh Автор
      23.12.2017 08:02

      Спасибо за рекомендацию доклада. Добавлю ваше замечание к статье.


  1. dmlukas
    24.12.2017 01:29

    Спасибо, отличная статья, а еще лучше комментарии ).


  1. kpcb
    24.12.2017 07:36

    Не являюсь android-разработчиком, но по-моему из-за subscribeOn(Schedulers.io()) установка listener-а на поле ввода будет происходить не в главном потоке


    1. tehreh1uneh Автор
      24.12.2017 07:45

      Насколько я знаю, subscribeOn() указывает в какой поток наблюдаемый источник (в нашем случае subject внутри listener поля ввода) будет передавать создаваемые observable элементы. То есть событие происходит в UI потоке, затем его отлавливает listener в этом же потоке, далее subject внутри listener создаёт событие (onNext()), а возвращаемый observable элемент уже попадает в поток IO.


      1. HotIceCream
        25.12.2017 00:33

        Мне почему-то кажется что прав kpcb.
        subscribeOn указывает поток в котором будет происходить подписка. И во время подписки вы создаете слушатель.
        Слушатель в UI потоке будет дергать subject.onNext(). и возвращаемый observable будет тоже получать событие в UI потоке.
        Но дальше у вас идет метод debounce. Это особенный метод, который возвращает observable, события которого будут производиться в computation sheduler. (Scheduler можно изменить — посмотрите перегрузку метода debounce).


        1. tehreh1uneh Автор
          25.12.2017 09:06

          Проверил. Subject.onNext() отрабатывает в UI потоке, а Debounce() уже в Computation. Спасибо за внимательность.