Как реализовать периодическую обработку ввода пользователя?
Мое текущее приложение для 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, прежде чем я отправлю код, я добавлю шаги, которые я делаю.
- добавьте объект PublishSubject, который будет принимать ваши входы и добавит к нему фильтр, который будет проверять, превышает ли вход два или нет.
- добавьте метод debounce, чтобы все входные события, которые запускались до 300 мс, игнорировались, и принимался во внимание окончательный запрос, который запускается после 300 мс.
- теперь добавьте коммутационную карту и добавьте в нее событие сетевого запроса,
- Подпишитесь на мероприятие.
Код выглядит следующим образом:
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 Это работает для меня !!