如何实现用户输入的周期性处理?

12

我的当前Android应用程序允许用户远程搜索内容。

例如,用户会看到一个EditText,接受他们的搜索字符串并触发一个远程API调用,返回与输入文本匹配的结果。

最坏的情况是我只需添加一个TextWatcher,每次调用onTextChanged时触发一个API调用。这可以通过强制用户在进行第一次API调用之前至少输入N个字符来改进。

“完美”的解决方案将具有以下功能:

一旦用户开始输入搜索字符串

每隔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 + "]");
                    }
                });

两种方法都不能给我一个组合输入文本长度和超时值的条件过滤器。

我已经用throttleLast和sample替换了debounce,但是它们都没有提供所需的解决方案。

是否有可能实现我所需的功能?

动态超时

可接受的解决方案应该应对以下三种情况:

i)用户希望搜索以“P”开头的任何单词

ii)用户希望搜索以“Pneumo”开头的任何单词

iii)用户希望搜索“Pneumonoultramicroscopicsilicovolcanoconiosis”这个词

在所有三种情况下,只要用户键入字母“P”,我就会显示进度旋转器(但此时不会执行任何API调用)。我想平衡在响应式UI中为用户提供搜索反馈的需求与在网络上进行“浪费”的API调用之间的关系。

如果我可以依赖于用户输入其搜索文本,然后单击“完成”(或“Enter”)键,我可以立即启动最终的API调用。

场景一

由于用户输入的文本长度较短(例如,只有1个字符),我的超时值将达到最大值,这为用户提供了输入其他字符的机会,并节省了“浪费的API调用”。

由于用户希望仅搜索字母“P”,因此一旦最大超时时间到期,我将执行API调用并显示结果。这种情况下,用户体验最差,因为他们必须等待我的动态超时到期,然后等待返回和显示大型API响应。他们不会看到任何中间搜索结果。

场景二

如果用户迅速输入所有6个字符,这种情况将结合场景1,我不知道用户要搜索什么(或搜索字符串的最终长度),但是我可以执行一个API调用,但是他们输入6个字符越慢,就越容易执行浪费的API调用。

这种情况给用户提供了更好的用户体验,因为他们必须等待我的动态超时到期,但是他们有机会看到中间搜索结果。与场景1相比,API响应将更小。

场景三

如果用户快速输入所有45个字符,这种情况将结合场景1和2,我不知道用户将要搜索什么(或搜索字符串的最终长度),但是我可以执行一个API调用(也许!),但是他们输入45个字符越慢,就越容易执行浪费的API调用。

我不会被任何可以实现所需解决方案的技术绑定。到目前为止,我认为Rx是最好的方法。


1
我认为如果文本很短但响应很大,那么你需要使用某种分页方式。 - y.allam
@y.allam 我离解决分页还有很长的路要走。我需要一个 RxBinding/RxJava 的解决方案,以满足我所有(或大多数)的最初需求。 - Hector
1
你能再解释一下“动态超时”吗?你想在响应到达之前显示一个加载器(对于短文本将显示更长时间,对于长文本将显示更短时间)吗? - RoyalGriffin
1
此外,是否可以接受不使用RxJava和/或RxBinding的解决方案? - RoyalGriffin
4个回答

8
像这样的内容应该可以工作(我没有真正尝试过)。
 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并不是一个随时间流动的流,而是使用RxBinding的InitialValueObservable属性对当前状态的EditText进行采样(它只是在订阅时发出EditText上的当前文本),换句话说,它是一种获取当前文本的高级方式,它等同于:
Observable.fromCallable(() -> mEditText.getText().toString());
然后,对于动态超时/延迟,我们根据文本长度更新nextDelay并使用计时器的repeatWhen等待所需时间。加上distinctUntilChanged,它应该根据文本长度提供所需的采样结果。接下来,我们将根据文本(如果长度足够长)发出请求。

按Enter键停止 - 使用takeUntilenterStream,它将在按Enter键时触发并且还将触发最终查询。

重新启动 - 当用户“重新启动”输入时,即文本为空时,.takeUntil(restartTypingStream)+ repeat()将在输入空字符串时停止流,并重新启动它(重新订阅)。


1

嗯,你可以使用类似这样的东西:

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

博客参考


1

你的问题可以通过使用RxJava2方法轻松解决,在我发布代码之前,我将添加我正在执行的步骤。

  1. 添加一个PublishSubject,它将接收您的输入并添加一个过滤器,检查输入是否大于两个。
  2. 添加防抖动方法,以便忽略在300ms之前触发的所有输入事件,并考虑在300ms后触发的最终查询。
  3. 现在添加switchmap并将您的网络请求事件添加到其中,
  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) {

        }
    });

P.S. 这对我有效!!


0

你可能会在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而不是任何对象。使用方式基本相同。请参阅此tutorial


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接