Let’s start with the basic logic:

Whenever the user types something we perform a network call and then we get the results:

RxTextView.textChanges(searchEditText)

.flatMap(Api::searchItems)

.subscribe(this::updateList, t->showError());

1. Reduce network requests.

The above has two problems:

you make a request per letter (yes that’s bad) e.g.: user types quickly “a”, then “ab” then “abc” then corrects to “ab” and want to search finally for “abe”. You have made 5 network requests. Imagine if there is slow internet connectivity. you have a threading race possible problem e.g: user types “a”, then “ab”. The network call for the “ab” comes first and the call for “a” second. In that case the last updateList() will get executed with results from the “a”.

Solutions:

Add throttling behaviour:

debounce() is what you usually need. Values between 100–150 millis from my experience work the best. If your server needs additionally another 300ms then you can do a UI update in under 0,5 seconds.

RxTextView.textChanges(searchEditText)

.debounce(150, MILLISECONDS)

.flatMap(Api::searchItems)

.subscribe(this::updateList, t->showError());

2. Kill the previous requests:

introduce switchMap instead of flatMap. It will stop the previously emitted items. So if in time 0+150ms you search for “ab” and in time 0+300ms you search for “abcd” but the “ab” network call needs more than 150ms to complete, then by the time you start the “abcd” call the previous one will get canceled and you will always have the most recent result.

RxTextView.textChanges(searchEditText)

.debounce(150, MILLISECONDS)

.switchMap(Api::searchItems)

.subscribe(this::updateList, t->showError());

2. No error functionality / no network functionality

If a network call fails, you will never again observe text changes.

This can be easily solved by adding some error catching functionality.

So you could just use:

RxTextView.textChanges(searchEditText)

.debounce(150, MILLISECONDS)

.switchMap(Api::searchItems)

.onErrorResumeNext(t-> empty())

.subscribe(this::updateList);

Don’t do that. Let’s make it smarter. What if the searchItems() api call above calls because of connectivity? Or even more “UX-depressingly” brief connectivity that the user didn’t notice?

You need a retry mechanism for these:

RxTextView.textChanges(searchEditText)

.debounce(150, MILLISECONDS)

.switchMap(Api::searchItems)

.retryWhen(new RetryWithConnectivity())

.subscribe(this::updateList, t->showError());

How to improve it even further? By adding a timeout. As our (car2go) UX designer Leander Lenzing says, “1 second is a lot of time in the user world”. So the above should be something like:

RxTextView.textChanges(searchEditText)

.debounce(150, MILLISECONDS)

.switchMap(Api::searchItems)

.retryWhen(new RetryWithConnectivityIncremental(context, 5, 15, SECONDS))

.subscribe(this::updateList, t->showErrorToUser());

So what will RetryWithConnectivityIncremental vs RetryWithConnectivity do? It will wait 5 seconds the phone to have internet, if more it will throw an exception. If the user retries it will wait with a longer timeout (e.g. 15 seconds).

Here is the code: