For my app's search feature, I have a hot observable chain that does the following.
EditText (a TextChangedEvent) (on mainThread)computation thread)mainThread)Schedulers.io())mainThread)Because step 3 is so variable in length, a race condition occurs where less recent search results are displayed over more recent results (sometimes). Let's say a user wants to type chicken, but because of weird typing speeds, the first part of the word is emitted before the whole term:
chick is sent out first, followed by chicken.chick takes 1500ms to execute while chicken takes 300ms to execute. chick search results to incorrectly display for the search term chicken. This is because the chicken search completed first (took only 300ms), followed by the chick search (1500ms).How can I handle this scenario?
TextChangedEvent I don't care about the old search, even if its still running. Is there any way to cancel the old search?Full observable code:
subscription = WidgetObservable.text(searchText)
.debounce(300, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
//do this on main thread because it's a UI element (cannot access a View from a background thread)
//get a String representing the new text entered in the EditText
.map(new Func1<OnTextChangeEvent, String>() {
@Override
public String call(OnTextChangeEvent onTextChangeEvent) {
return onTextChangeEvent.text().toString().trim();
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.doOnNext(new Action1<String>() {
@Override
public void call(String s) {
presenter.handleInput(s);
}
})
.subscribeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.filter(new Func1<String, Boolean>() {
@Override
public Boolean call(String s) {
return s != null && s.length() >= 1 && !s.equals("");
}
}).doOnNext(new Action1<String>() {
@Override
public void call(String s) {
Timber.d("searching for string: '%s'", s);
}
})
//run SQL query and get a cursor for all the possible search results with the entered search term
.flatMap(new Func1<String, Observable<SearchBookmarkableAdapterViewModel>>() {
@Override
public Observable<SearchBookmarkableAdapterViewModel> call(String s) {
return presenter.getAdapterViewModelRx(s);
}
})
.subscribeOn(Schedulers.io())
//have the subscriber (the adapter) run on the main thread
.observeOn(AndroidSchedulers.mainThread())
//subscribe the adapter, which receives a stream containing a list of my search result objects and populates the view with them
.subscribe(new Subscriber<SearchBookmarkableAdapterViewModel>() {
@Override
public void onCompleted() {
Timber.v("Completed loading results");
}
@Override
public void onError(Throwable e) {
Timber.e(e, "Error loading results");
presenter.onNoResults();
//resubscribe so the observable keeps working.
subscribeSearchText();
}
@Override
public void onNext(SearchBookmarkableAdapterViewModel searchBookmarkableAdapterViewModel) {
Timber.v("Loading data with size: %d into adapter", searchBookmarkableAdapterViewModel.getSize());
adapter.loadDataIntoAdapter(searchBookmarkableAdapterViewModel);
final int resultCount = searchBookmarkableAdapterViewModel.getSize();
if (resultCount == 0)
presenter.onNoResults();
else
presenter.onResults();
}
});
Use switchMap instead of flatMap. That will cause it to throw away* the previous query whenever you start a new query.
*How this works:
Whenever the outer source observable produces a new value, switchMap calls your selector to return a new inner observable (presenter.getAdapterViewModelRx(s) in this case). switchMap then unsubscribes from the previous inner observable it was listening to and subscribes to the new one.
Unsubscribing from the previous inner observable has two effects:
Any notification (value, completion, error, etc) produced by the observable will be silently ignored and thrown away.
The observable will be notified that its observer has unsubscribed and can optionally take steps to cancel whatever asynchronous process it represents.
Whether your abandoned queries are actually cancelled or not is entirely dependent upon the implementation of presenter.getAdapterViewModelRx(). Ideally they would be canceled to avoid needlessly wasting server resources. But even if they keep running, #1 above prevents your typeahead code from seeing stale results.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With