Как реализовать периодическую обработку пользовательского ввода?

Мое текущее приложение для Android позволяет пользователям удаленно искать контент.

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

В худшем случае я просто добавляю TextWatcher и запускаю вызов API каждый раз, когда вызывается onTextChanged. Это можно улучшить, заставив пользователя вводить не менее N символов для поиска перед выполнением первого вызова API.

«Идеальное» решение будет иметь следующие особенности:

Как только пользователь начинает вводить строку(и) поиска

Периодически (каждые M миллисекунд) использовать всю введенную строку (строки). Запускайте вызов API каждый раз, когда истекает период, а текущий ввод пользователя отличается от предыдущего ввода пользователя.

[Возможно ли иметь динамический тайм-аут, связанный с длиной введенного текста? например, пока текст «короткий», размер ответа API будет большим, и для возврата и анализа потребуется больше времени; По мере того, как текст поиска становится длиннее, размер ответа API будет уменьшаться вместе с «в процессе» и временем синтаксического анализа]

Когда пользователь перезапустит ввод текста в поле EditText, перезапустите периодическое потребление текста.

Всякий раз, когда пользователь нажимает клавишу ENTER, инициируется «последний» вызов API и прекращается отслеживание пользовательского ввода в поле EditText.

Установите минимальную длину текста, который пользователь должен ввести, прежде чем будет запущен вызов API, но объедините эту минимальную длину с переопределяющим значением тайм-аута, чтобы, когда пользователь хочет найти «короткую» текстовую строку, он мог это сделать.

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

Мои попытки включают

private PublishSubject<String> publishSubject;

  publishSubject = PublishSubject.create();
        publishSubject.filter(text -> text.length() > 2)
                .debounce(300, TimeUnit.MILLISECONDS)
                .toFlowable(BackpressureStrategy.LATEST)
                .subscribe(new Consumer<String>() {
                    @Override
                    public void accept(final String s) throws Exception {
                        Log.d(TAG, "accept() called with: s = [" + s + "]");
                    }
                });


       mEditText.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(final CharSequence s, final int start, final int count, final int after) {

            }

            @Override
            public void onTextChanged(final CharSequence s, final int start, final int before, final int count) {
                publishSubject.onNext(s.toString());
            }

            @Override
            public void afterTextChanged(final Editable s) {

            }
        });

И это с RxBinding

 RxTextView.textChanges(mEditText)
                .debounce(500, TimeUnit.MILLISECONDS)
                .subscribe(new Consumer<CharSequence>(){
                    @Override
                    public void accept(final CharSequence charSequence) throws Exception {
                        Log.d(TAG, "accept() called with: charSequence = [" + charSequence + "]");
                    }
                });

Ни один из них не дает мне условный фильтр, который сочетает в себе введенную длину текста и значение тайм-аута.

Я также заменил debounce на ThrottleLast и образец, ни один из которых не дал требуемого решения.

Можно ли достичь требуемой функциональности?

ДИНАМИЧЕСКИЙ ТАЙМ-АУТ

Приемлемое решение справится со следующими тремя сценариями

я). Пользователь хочет найти любое слово, начинающееся с «P».

ii). Пользователь хочет найти любое слово, начинающееся с «Пневмо».

III). Пользователь желает выполнить поиск по слову «пневмоноультрамикроскопический силиковулканокониоз».

Во всех трех сценариях, как только пользователь введет букву «P», я покажу счетчик прогресса (однако в этот момент вызов API выполняться не будет). Я хотел бы сбалансировать необходимость предоставления пользователю отзывов о поиске в адаптивном пользовательском интерфейсе и «бесполезных» вызовов API по сети.

Если бы я мог положиться на то, что пользователь вводит текст поиска, а затем нажимает клавишу «Готово» (или «Ввод»), я мог бы немедленно инициировать последний вызов API.

Первый сценарий

Поскольку текст, введенный пользователем, имеет короткую длину (например, 1 символ), мое значение тайм-аута будет максимальным. Это дает пользователю возможность вводить дополнительные символы и экономит «бесполезные вызовы API».

Поскольку пользователь хочет искать только букву «P», по истечении максимального времени ожидания я выполню вызов API и отобразлю результаты. Этот сценарий дает пользователю наихудший пользовательский опыт, поскольку ему приходится ждать истечения срока действия моего динамического тайм-аута, а затем ждать возврата и отображения ответа Large API. Они не увидят промежуточных результатов поиска.

Сценарий 2

Этот сценарий объединяет сценарий один, поскольку я понятия не имею, что пользователь будет искать (или конечную длину строки поиска), если он наберет все 6 символов «быстро», я могу выполнить один вызов API, однако чем медленнее они вводят 6 символы увеличат вероятность выполнения напрасных вызовов API.

Этот сценарий дает пользователю улучшенный пользовательский опыт, поскольку ему нужно дождаться истечения моего динамического тайм-аута, однако у него есть шанс увидеть промежуточные результаты поиска. Ответы API будут меньше, чем в первом сценарии.

Сценарий третий

Этот сценарий сочетает в себе сценарий один и два, поскольку я понятия не имею, что пользователь будет искать (или конечную длину строки поиска), если он наберет все 45 символов «быстро», я могу выполнить один вызов API (возможно!), однако чем медленнее они набирают 45 символов, тем выше вероятность выполнения бесполезных вызовов API.

Я не привязан к какой-либо технологии, которая обеспечивает желаемое решение. Я считаю, что Rx — лучший подход, который я определил до сих пор.


person Hector    schedule 14.03.2018    source источник
comment
я думаю, что если ответ такой большой, а текст короткий, то вам нужно использовать какую-то нумерацию страниц   -  person y.allam    schedule 14.03.2018
comment
@y.allam Мне далеко до нумерации страниц. Мне нужно получить решение RxBinding/RxJava для удовлетворения всех (или большинства) моих первоначальных требований.   -  person Hector    schedule 14.03.2018
comment
Можете ли вы объяснить немного больше о динамическом тайм-ауте? Вы хотите показать загрузчик, пока приходит ваш ответ (который будет отображаться дольше в случае коротких текстов и меньше времени в случае длинных текстов)?   -  person RoyalGriffin    schedule 22.03.2018
comment
Кроме того, приемлемо ли решение без RxJava и/или RxBinding?   -  person RoyalGriffin    schedule 22.03.2018


Ответы (4)


Что-то вроде этого должно работать (на самом деле не пробовал)

 Single<String> firstTypeOnlyStream = RxTextView.textChanges(mEditText)
            .skipInitialValue()
            .map(CharSequence::toString)
            .firstOrError();

    Observable<CharSequence> restartTypingStream = RxTextView.textChanges(mEditText)
            .filter(charSequence -> charSequence.length() == 0);

    Single<String> latestTextStream = RxTextView.textChanges(mEditText)
            .map(CharSequence::toString)
            .firstOrError();

    Observable<TextViewEditorActionEvent> enterStream =
            RxTextView.editorActionEvents(mEditText, actionEvent -> actionEvent.actionId() == EditorInfo.IME_ACTION_DONE);

    firstTypeOnlyStream
            .flatMapObservable(__ ->
                    latestTextStream
                            .toObservable()
                            .doOnNext(text -> nextDelay = delayByLength(text.length()))
                            .repeatWhen(objectObservable -> objectObservable
                                    .flatMap(o -> Observable.timer(nextDelay, TimeUnit.MILLISECONDS)))
                            .distinctUntilChanged()
                            .flatMap(text -> {
                                if (text.length() > MINIMUM_TEXT_LENGTH) {
                                    return apiRequest(text);
                                } else {
                                    return Observable.empty();
                                }
                            })
            )
            .takeUntil(restartTypingStream)
            .repeat()
            .takeUntil(enterStream)
            .mergeWith(enterStream.flatMap(__ ->
                    latestTextStream.flatMapObservable(this::apiRequest)
            ))
            .subscribe(requestResult -> {
                //do your thing with each request result
            });

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

То, как я сделал это здесь, состоит в том, чтобы создать один поток (firstTypeOnlyStream для начального запуска событий (первый ввод текста пользователем), этот поток запустит весь поток обработки с первого ввода пользователя, затем, когда это При поступлении первого триггера мы будем периодически сэмплировать текст редактирования, используя latestTextStream. latestTextStream на самом деле не поток с течением времени, а скорее выборка текущего состояния EditText с использованием свойства InitialValueObservable RxBinding (он просто выдает при подписке текущий текст на EditText) другими словами, это причудливый способ получить текущий текст при подписке, и он эквивалентен:
Observable.fromCallable(() -> mEditText.getText().toString());
далее, для динамического тайм-аута/задержки, мы обновляем nextDelay на основе длины текста и использования repeatWhen с таймером ждать нужное время, вместе с distinctUntilChanged должно давать желаемую выборку на основе длины текста, далее мы будем запускать запрос на основе текста (если он достаточно длинный).

Stop by Enter — используйте takeUntil с enterStream, которые будут срабатывать при Enter, а также запускать окончательный запрос.

Перезапуск — когда пользователь «перезапускает» набор текста — т. е. текст пуст, .takeUntil(restartTypingStream) + repeat() остановит поток при вводе пустой строки и перезапустит его (повторная подписка).

person yosriz    schedule 18.03.2018

Ну, вы могли бы использовать что-то вроде этого:

RxSearch.fromSearchView(searchView)
            .debounce(300, TimeUnit.MILLISECONDS)
            .filter(item -> item.length() > 1)
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(query -> {
                adapter.setNamesList(namesAPI.searchForName(query));
                adapter.notifyDataSetChanged();
                apiCallsTextView.setText("API CALLS: " + apiCalls++);
            });    

public class RxSearch {
    public static Observable<String> fromSearchView(@NonNull final SearchView searchView) {
        final BehaviorSubject<String> subject = BehaviorSubject.create("");

        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {

            @Override
            public boolean onQueryTextSubmit(String query) {
                subject.onCompleted();
                return true;
            }

            @Override
            public boolean onQueryTextChange(String newText) {
                if (!newText.isEmpty()) {
                    subject.onNext(newText);
                }
                return true;
            }
        });

        return subject;
    }
}

ссылка на блог

person João Carlos    schedule 21.03.2018

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

  1. добавьте PublishSubject, который будет принимать ваши входные данные, и добавьте к нему фильтр, который будет проверять, больше ли входных данных два или нет.
  2. добавьте метод debounce, чтобы все входные события, которые запускаются до 300 мс, игнорировались, а окончательный запрос, который запускается после 300 мс, принимался во внимание.
  3. теперь добавьте карту переключения и добавьте в нее событие сетевого запроса,
  4. Подпишитесь на ваше мероприятие.

Код выглядит следующим образом:

subject = PublishSubject.create(); //add this inside your oncreate
    getCompositeDisposable().add(subject
            .doOnEach(stringNotification -> {
                if(stringNotification.getValue().length() < 3) {
                    getMvpView().hideEditLoading();
                    getMvpView().onFieldError("minimum 3 characters required");
                }
            })
            .debounce(300,
                    TimeUnit.MILLISECONDS)
            .filter(s -> s.length() >= 3)
            .switchMap(s -> getDataManager().getHosts(
                    getDataManager().getDeviceToken(),
                    s).subscribeOn(Schedulers.io()))
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(hostResponses -> {
                getMvpView().hideEditLoading();
                if (hostResponses.size() != 0) {
                    if (this.hostResponses != null)
                        this.hostResponses.clear();
                    this.hostResponses = hostResponses;
                    getMvpView().setHostView(getHosts(hostResponses));
                } else {
                    getMvpView().onFieldError("No host found");
                }

            }, throwable -> {
                getMvpView().hideEditLoading();
                if (throwable instanceof HttpException) {
                    HttpException exception = (HttpException) throwable;
                    if (exception.code() == 401) {
                        getMvpView().onError(R.string.code_expired,
                                BaseUtils.TOKEN_EXPIRY_TAG);
                    }
                }

            })
);

это будет ваш текстовый наблюдатель:

searchView.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) {
            subject.onNext(charSequence.toString());
        }

        @Override
        public void afterTextChanged(Editable editable) {

        }
    });

P.S. Это работает для меня!!

person Abhishek Tiwari    schedule 23.03.2018

Вы можете найти то, что вам нужно, в as оператор. Требуется ObservableConverter, который позволяет вам преобразовать ваш источник Observable в произвольный объект. Этот объект может быть другим Observable с произвольно сложным поведением.

public class MyConverter implements ObservableConverter<Foo, Observable<Bar>> {
    Observable<Bar> apply(Observable<Foo> upstream) {
        final PublishSubject<Bar> downstream = PublishSubject.create();
        // subscribe to upstream
        // subscriber publishes to downstream according to your rules
        return downstream;
    }
}

Затем используйте его следующим образом:

someObservableOfFoo.as(new MyConverter())... // more operators

Изменить: я думаю, что compose может быть более парадигматическим. Это менее мощная версия as специально для создания Observable вместо любого объекта. Использование по сути такое же. См. это руководство.

person StackOverthrow    schedule 14.03.2018