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

时间:2018-03-14 12:17:01

标签: android rx-java2 rx-android rx-binding

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

e.g。向用户显示EditText,该TextWatcher接受其搜索字符串并触发远程API调用,该调用返回与输入文本匹配的结果。

更糟糕的情况是,我只需添加onTextChanged并在每次调用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) { } }); 时触发API调用。这可以通过在进行第一次API调用之前强制用户输入至少N个字符来进行搜索来改进。

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

用户开始输入搜索字符串

定期(每M毫秒)消耗输入的整个字符串。每当周期到期且当前用户输入与先前的用户输入不同时触发API调用。

[是否可以设置与输入的文本长度相关的动态超时?例如,虽然文本是“短”的,但API响应大小会很大并且需要更长的时间来返回和解析;随着搜索文本变得越来越长,API响应大小将随着“飞行时间”和解析时间而减少

当用户重新键入EditText字段时,重新开始定期消费文本。

每当用户按下ENTER键触发“final”API调用,并停止监视用户输入到EditText字段。

设置用户在触发API调用之前必须输入的文本的最小长度,但将此最小长度与覆盖的超时值组合,以便当用户希望搜索“短”文本字符串时,他们可以。

我确信RxJava和/或RxBindings可以支持上述要求,但到目前为止我还未能实现可行的解决方案。

我的尝试包括

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

这与RxBinding

{{1}}

这些都没有给我一个条件过滤器,它结合了输入的文本长度和超时值。

我还用throttleLast替换了debounce,并且样本都没有提供所需的解决方案。

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

DYNAMIC TIMEOUT

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

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

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

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

在所有三种情况下,一旦用户键入字母“P”,我将显示进度微调器(但此时不会执行API调用)。我想平衡在响应式用户界面中提供用户搜索反馈的需求,而不是通过网络进行“浪费”的API调用。

如果我可以依赖用户输入他们的搜索文本然后点击“完成”(或“输入”)键,我可以立即发起最终的API调用。

场景一

由于用户输入的文本长度很短(例如,1个字符长),我的超时值将达到其最大值,这使用户有机会输入其他字符并保存“浪费的API调用”。

由于用户希望单独搜索字母“P”,一旦Max Timeout到期,我将执行API调用并显示结果。 此方案为用户提供最差的用户体验,因为他们必须等待我的Dynamic Timeout过期,然后等待返回并显示Large API响应。他们不会看到任何中间搜索结果。

场景二

这个场景结合了场景一,因为我不知道用户要搜索的内容(或搜索字符串的最终长度),如果他们“快速”键入所有6个字符,我可以执行一个API调用,但是它们越慢输入6个字符将增加执行浪费的API调用的机会。

此方案为用户提供了改进的用户体验,因为他们必须等待我的动态超时过期,但他们确实有机会看到中间搜索结果。 API响应将小于方案一。

场景三

这个场景结合了场景一和二,因为我不知道用户要搜索什么(或搜索字符串最终长度)如果他们“快速”键入所有45个字符我可以执行一个API调用(可能!)但是,输入45个字符的速度越慢,就越有可能增加执行API调用的可能性。

我与任何提供我所需解决方案的技术无关。我相信Rx是迄今为止我发现的最佳方法。

4 个答案:

答案 0 :(得分: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实际上并不是一段时间内的流,而是对当前状态的采样。 EditText使用RxBinding的InitialValueObservable属性(它只是在订阅时发出EditText上的当前文本),换句话说,它是获取订阅时当前文本的一种奇特方式,并且它等同于:
  Observable.fromCallable(() -> mEditText.getText().toString());
接下来,对于动态超时/延迟,我们根据文本长度更新nextDelay并使用带有计时器的repeatWhen等待所需的时间。与distinctUntilChanged一起,它应该根据文本长度给出所需的抽样。接下来,我们将根据文本触发请求(如果足够长的话)。

停止输入 - 将takeUntilenterStream一起使用,这将在Enter上触发,它也会触发最终查询。

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

答案 1 :(得分: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

答案 2 :(得分:0)

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

Request request;
Response response = null;

  try {

    response =
      client
        .newCall(request)
        .execute();

  } catch (IOException e) {

    response = ... // Need to instantiate manually.
  }

blog referencia

答案 3 :(得分:0)

您的查询可以通过使用RxJava2方法轻松解决,在我发布代码之前,我将添加我正在做的步骤。

  1. 添加一个PublishSubject,它将接收您的输入并为其添加一个过滤器,用于检查输入是否大于2。
  2. 添加debounce方法,以便忽略在300ms之前触发的所有输入事件,并考虑在300ms之后触发的最终查询。
  3. 现在添加一个switchmap并将您的网络请求事件添加到其中,
  4. 订阅您的活动。
  5. 代码如下:

    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。这对我有用!!