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

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

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

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

Решение "Perfect" будет иметь следующие функции:

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

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

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

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

Всякий раз, когда пользователь нажимает клавишу вызова "конечный" API кнопки "ENTER" и останавливает мониторинг ввода пользователя в поле 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). Пользователь хочет найти любое слово, начинающееся с "Pneumo"

III). Пользователь хочет найти слово "Pneumonoultramicroscopicsilicovolcanoconiosis"

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

Если бы я мог полагаться на пользователя, вводящего его текст поиска, а затем нажав кнопку "Готово" (или "Enter"), я мог бы немедленно запустить окончательный вызов API.

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

Так как текст, введенный пользователем, коротким (например, длиной 1 символ). Мое значение тайм-аута будет иметь максимальное значение. Это дает пользователю возможность вводить дополнительные символы и сохраняет "потерянные API-вызовы".

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

Сценарий два

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

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

Сценарий три

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

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

Ответы

Ответ 1

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

 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, он должен дать желаемую выборку на основе длины текста. далее, мы будем уволить запрос на основе текста (если достаточно долго).

Остановить с помощью Enter - использовать takeUntil с enterStream который будет активирован при enterStream а также инициирует окончательный запрос.

Перезапуск - когда пользователь "перезапускает" ввод, т. .takeUntil(restartTypingStream) Текст пуст, .takeUntil(restartTypingStream) + repeat() останавливает поток при вводе пустой строки и перезапускает его (переписывает).

Ответ 2

Вы можете найти то, что вам нужно в 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 а не для любого объекта. Использование по сути то же самое. См. Этот учебник.

Ответ 3

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

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;
    }
}

блог referencia

Ответ 4

ваш запрос можно легко решить с помощью методов 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);
                    }
                }

            })
);

это будет ваш textwatcher:

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) {

        }
    });

PS Это работает для меня !!