Reputation: 12467
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();
}
});
Upvotes: 3
Views: 1914
Reputation: 39192
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.
Upvotes: 3