如何实现用户输入的定期处理?
How to implement Periodic processing of user input?
我当前的 Android 应用程序允许用户远程搜索内容。
例如用户会看到一个 EditText
,它接受他们的搜索字符串并触发一个远程 API 调用,return 的结果与输入的文本相匹配。
更糟糕的情况是我只是添加一个 TextWatcher
并在每次调用 onTextChanged
时触发一个 API 调用。这可以通过强制用户在进行第一次 API 调用之前至少输入 N 个字符进行搜索来改进。
"Perfect" 解决方案具有以下特点:-
一旦用户开始输入搜索字符串
定期(每 M 毫秒)消耗输入的整个字符串。每次周期到期并且当前用户输入与之前的用户输入不同时触发 API 调用。
[是否可以设置与输入文本长度相关的动态超时?例如,当文本是 "short" 时,API 响应大小会很大并且需要更长的时间来 return 和解析;随着搜索文本变长,API 响应大小将随 "inflight" 和解析时间一起减少]
当用户重新在 EditText 字段中键入时,重新启动文本的定期消耗。
每当用户按下 ENTER 键时触发 "final" API 调用,并停止监视用户在 EditText 字段中的输入。
设置用户在触发 API 调用之前必须输入的最小文本长度,但将此最小长度与覆盖超时值相结合,以便当用户希望搜索 "short" 文本字符串。
我确定 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 + "]");
}
});
两者都没有给我结合输入的文本长度和超时值的条件过滤器。
我还用 throttleLast 和 sample 替换了 debounce,两者都没有提供所需的解决方案。
是否可以实现我需要的功能?
动态超时
一个可接受的解决方案将应对以下三种情况
我)。用户希望搜索以 "P"
开头的任意单词
ii).用户希望搜索任何以 "Pneumo"
开头的单词
iii).用户希望搜索词"Pneumonoultramicroscopicsilicovolcanoconiosis"
在所有三种情况下,只要用户键入字母 "P",我就会显示进度微调器(但是此时不会执行任何 API 调用)。我想平衡在响应式 UI 中向用户提供搜索反馈与通过网络进行 "wasted" API 调用之间的平衡。
如果我可以依靠用户输入他们的搜索文本然后单击 "Done"(或 "Enter")键,我可以立即启动最后的 API 调用。
场景一
由于用户输入的文本长度较短(例如 1 个字符长),我的超时值将处于最大值,这使用户有机会输入其他字符并节省 "wasted API calls"。
由于用户希望单独搜索字母 "P",一旦 Max Timeout 到期,我将执行 API 调用并显示结果。
这种情况给用户带来了最糟糕的用户体验,因为他们必须等待我的动态超时到期,然后等待 API 大响应被 return 编辑和显示。他们不会看到任何中间搜索结果。
场景二
这个场景结合了场景一,因为我不知道用户要搜索什么(或搜索字符串的最终长度),如果他们键入所有 6 个字符 "quickly" 我可以执行一个 API 调用,但是他们输入 6 个字符的速度越慢,将增加执行浪费的 API 调用的机会。
此方案为用户提供了改进的用户体验,因为他们必须等待我的动态超时到期,但他们确实有机会看到中间搜索结果。 API 响应将小于方案一。
场景三
这个场景结合了场景一和场景二,因为我不知道用户要搜索什么(或搜索字符串的最终长度)如果他们键入全部 45 个字符"quickly" 我可以执行一个 API 调用(也许!),但是他们键入 45 个字符的速度越慢,执行浪费的 API 调用的机会就会增加。
我不受任何提供我想要的解决方案的技术的束缚。我相信 Rx 是迄今为止我发现的最好的方法。
您可能会在 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
may be more paradigmatic. It's a less powerful version of as
specifically for producing an Observable
instead of any object. Usage is essentially the same. See this tutorial。
像这样的东西应该行得通(没有真正尝试过)
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
使用 RxBinding 的 InitialValueObservable
属性 (它只是在订阅时发出 EditText
上的当前文本)换句话说,这是一种获取当前文本的奇特方式订阅,相当于:
Observable.fromCallable(() -> mEditText.getText().toString());
接下来,对于动态 timeout/delay,我们根据文本长度更新 nextDelay
,并使用带有计时器的 repeatWhen
来等待所需的时间。与 distinctUntilChanged
一起,它应该根据文本长度给出所需的采样。进一步,我们将根据文本(如果足够长)触发请求。
按 Enter 停止 - 使用 takeUntil
和 enterStream
,这将在 Enter 时触发,它也会触发最终查询。
Restarting - 当用户 'restarts' 键入 - 即文本为空时,.takeUntil(restartTypingStream)
+ repeat()
将在空字符串时停止流输入,然后重新启动它(重新订阅)。
嗯,你可以使用这样的东西:
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;
}
}
您的查询可以通过使用 RxJava2 方法轻松解决,在我 post 代码之前,我将添加我正在做的步骤。
- 添加一个 PublishSubject 来获取您的输入并向其添加一个过滤器以检查输入是否大于 2。
- 添加去抖动方法,以便忽略所有在 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);
}
}
})
);
这将是您的文字观察者:
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。这对我有用!!
我当前的 Android 应用程序允许用户远程搜索内容。
例如用户会看到一个 EditText
,它接受他们的搜索字符串并触发一个远程 API 调用,return 的结果与输入的文本相匹配。
更糟糕的情况是我只是添加一个 TextWatcher
并在每次调用 onTextChanged
时触发一个 API 调用。这可以通过强制用户在进行第一次 API 调用之前至少输入 N 个字符进行搜索来改进。
"Perfect" 解决方案具有以下特点:-
一旦用户开始输入搜索字符串
定期(每 M 毫秒)消耗输入的整个字符串。每次周期到期并且当前用户输入与之前的用户输入不同时触发 API 调用。
[是否可以设置与输入文本长度相关的动态超时?例如,当文本是 "short" 时,API 响应大小会很大并且需要更长的时间来 return 和解析;随着搜索文本变长,API 响应大小将随 "inflight" 和解析时间一起减少]
当用户重新在 EditText 字段中键入时,重新启动文本的定期消耗。
每当用户按下 ENTER 键时触发 "final" API 调用,并停止监视用户在 EditText 字段中的输入。
设置用户在触发 API 调用之前必须输入的最小文本长度,但将此最小长度与覆盖超时值相结合,以便当用户希望搜索 "short" 文本字符串。
我确定 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 + "]");
}
});
两者都没有给我结合输入的文本长度和超时值的条件过滤器。
我还用 throttleLast 和 sample 替换了 debounce,两者都没有提供所需的解决方案。
是否可以实现我需要的功能?
动态超时
一个可接受的解决方案将应对以下三种情况
我)。用户希望搜索以 "P"
开头的任意单词ii).用户希望搜索任何以 "Pneumo"
开头的单词iii).用户希望搜索词"Pneumonoultramicroscopicsilicovolcanoconiosis"
在所有三种情况下,只要用户键入字母 "P",我就会显示进度微调器(但是此时不会执行任何 API 调用)。我想平衡在响应式 UI 中向用户提供搜索反馈与通过网络进行 "wasted" API 调用之间的平衡。
如果我可以依靠用户输入他们的搜索文本然后单击 "Done"(或 "Enter")键,我可以立即启动最后的 API 调用。
场景一
由于用户输入的文本长度较短(例如 1 个字符长),我的超时值将处于最大值,这使用户有机会输入其他字符并节省 "wasted API calls"。
由于用户希望单独搜索字母 "P",一旦 Max Timeout 到期,我将执行 API 调用并显示结果。 这种情况给用户带来了最糟糕的用户体验,因为他们必须等待我的动态超时到期,然后等待 API 大响应被 return 编辑和显示。他们不会看到任何中间搜索结果。
场景二
这个场景结合了场景一,因为我不知道用户要搜索什么(或搜索字符串的最终长度),如果他们键入所有 6 个字符 "quickly" 我可以执行一个 API 调用,但是他们输入 6 个字符的速度越慢,将增加执行浪费的 API 调用的机会。
此方案为用户提供了改进的用户体验,因为他们必须等待我的动态超时到期,但他们确实有机会看到中间搜索结果。 API 响应将小于方案一。
场景三
这个场景结合了场景一和场景二,因为我不知道用户要搜索什么(或搜索字符串的最终长度)如果他们键入全部 45 个字符"quickly" 我可以执行一个 API 调用(也许!),但是他们键入 45 个字符的速度越慢,执行浪费的 API 调用的机会就会增加。
我不受任何提供我想要的解决方案的技术的束缚。我相信 Rx 是迄今为止我发现的最好的方法。
您可能会在 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
may be more paradigmatic. It's a less powerful version of as
specifically for producing an Observable
instead of any object. Usage is essentially the same. See this tutorial。
像这样的东西应该行得通(没有真正尝试过)
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
使用 RxBinding 的 InitialValueObservable
属性 (它只是在订阅时发出 EditText
上的当前文本)换句话说,这是一种获取当前文本的奇特方式订阅,相当于:
Observable.fromCallable(() -> mEditText.getText().toString());
接下来,对于动态 timeout/delay,我们根据文本长度更新 nextDelay
,并使用带有计时器的 repeatWhen
来等待所需的时间。与 distinctUntilChanged
一起,它应该根据文本长度给出所需的采样。进一步,我们将根据文本(如果足够长)触发请求。
按 Enter 停止 - 使用 takeUntil
和 enterStream
,这将在 Enter 时触发,它也会触发最终查询。
Restarting - 当用户 'restarts' 键入 - 即文本为空时,.takeUntil(restartTypingStream)
+ repeat()
将在空字符串时停止流输入,然后重新启动它(重新订阅)。
嗯,你可以使用这样的东西:
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;
}
}
您的查询可以通过使用 RxJava2 方法轻松解决,在我 post 代码之前,我将添加我正在做的步骤。
- 添加一个 PublishSubject 来获取您的输入并向其添加一个过滤器以检查输入是否大于 2。
- 添加去抖动方法,以便忽略所有在 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);
}
}
})
);
这将是您的文字观察者:
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。这对我有用!!