Search Like a Boss

Very simple instant search UI

Instant Search Using RxJava2

Purpose: While I was searching for the best practice of instant search implementations with RxJava, I said yea it’s heaven. There are lots of samples. I used one of these examples with a few changes and the result was horrible. PO was sad, I was sad, Users were sad. Those examples weren’t designed for production application and millions of users. Those were just some kind of bootstrap.

What where the problems they didn’t cover ?

  • Retry and Error Handling : You have an smile on your lips and you are typing on your phone to search some shit but an error occurs. What do you expect? the minimum expectation is to retry your query but those samples simply dispose and if you continue to type, you will not see a thing.
  • Interrupt and Cancel Last Requests : We are talking about instant search, While user is typing we may send undesired requests to the server. For example if user wanted to search “Star Wars” if user typed slowly to reach the wait threshold app might send both “Star wa” and ” Star wars” requests. Preferable behavior is to cancel the “Star wa” query request to prevent from unwanted concurrent results. Those samples simply just send a new request per query or funny thing happens : they will throw java.io.InterruptedIOException exception and due to the lack of exception handling, search will break.
  • Clear Text Issue : a Standard search box most of the times contains a “Clear” button which clears all the characters and makes edit text ready to accept new input. This bug will occur when user types a word like “Star Wars” so fast and while the request is sent, user clicks on the clear button. button action makes edit text clean and removes old results but after a while result of “Star Wars” request comes and the UI suddenly shows results of “Star Wars” phrase. Funny? Not in a deadline.

I mentioned bugs I faced into during implementation, so you have a sense about reasons of codes in Rx chain or maybe you offer a better approach. I skip boilerplate codes and I stick to the concept, you can see full sample on GitHub repository.

GitHub Repo Link : https://github.com/alizeyn/RxSearchSample

Here you can see what the whole search code really is, and I describe more details about every operator below.

RxTextView.textChanges(searchEditText)
                .map(CharSequence::toString)
                .doOnNext(this::showSearchViews)
                .filter(string -> string.length() >= MIN_CHAR_TO_SEARCH)
                .debounce(SEARCH_QUERY_DELAY, TimeUnit.MILLISECONDS)
                .subscribeOn(Schedulers.io())
                .switchMap(api::search)
                .retry()
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::next, Throwable::printStackTrace);

  • RxTextView.textChanges(searchEditText) RxTextView is an UI componenet from RxAndroid library, emits any change from textView or EditText (which is a TextView).
  • map(CharSequence::toString) RxTextView emits value as CharSequence and by map operation it changes the CharSequence to the Observable type we need which is a String.
  • doOnNext(this::showSearchViews) This is how we handle “Clear Text Issue”
    private void showSearchViews(String s) {
        if (s.length() > MIN_CHAR_TO_SEARCH) {
            searchRecyclerView.setVisibility(View.VISIBLE);
        } else {
            adapter.updateData(Collections.emptyList());
            searchRecyclerView.setVisibility(View.GONE);
        }
    }

Before filtering observable with length less than two characters, we should handle UI state. However we don’t need to request for these phrases but we may change UI corresponding to the phrase length. Imagine the situation which user clicks on the clear button just after last request is sent. Now when the invalid result comes out, the recyclerView has gone visibility.

  • filter(string -> string.length() >= MIN_CHAR_TO_SEARCH) Searching for queries less than two characters is not acceptable. You may prefer to search for one character too.
  • debounce(SEARCH_QUERY_DELAY, TimeUnit.MILLISECONDS) User is typing and typing takes time. debounce with a reasonable delay waits for user to finish typing then requests if this delay is not completed and user continue to type last request is cancelled so it reduces pressure from server and seems necessary to keep your server up.
  • subscribeOn(Schedulers.io()) SubscribeOn tells RxJava to create our observable in io thread. Read more about subscribeOn and observeOn.
  • switchMap(api::search)
@GET("api/v1/movies")
Observable<SearchResponse> search(@Query("q") String name);

Maps user query string to result from server. The reason for selecting SwitchMap over flatMap (or Map) is that in swtichMap if observable emits something new, previous emissions are no longer produce mapped observables this is an effective way to avoid stale results.

  • retry() When you’re implementing a service which user is interacting to it for a long time, Error handling is fucking important. If something went wrong user should still be able to interact to application. To approach this purpose it’s enough to call retrty(). Keep in mind which in RxJava onError is not something that you may do something after that. Actually when you get an Error and onError is called your observable is disposed and you’re not getting any emission. You should handle errors before that error happens.

I want to clear up something that many RxJava beginners get wrong: onError is an extreme event that should be reserved for times when sequences cannot continue. It means that there was a problem in processing the current item such that no future processing of any items can occur.

https://blog.danlew.net/2015/12/08/error-handling-in-rxjava/
  • observeOn(AndroidSchedulers.mainThread()) Result (List of Search result) should be emitted in the main thread for UI interaction. For example show the result in RecyclerView or produce the right message to user.
  • subscribe(this::next, Throwable::printStackTrace) As onComplete and onSubscribe is not needed I implemented just onNext and onError.
    private void next(SearchResponse searchResponse) {
        List<Movie> data = searchResponse.getData();
        if (data != null) {
            adapter.updateData(data);

            if (data.size() == 0) {
                Toast.makeText(SearchActivity.this,
                        "no result",
                        Toast.LENGTH_SHORT).show();
            }
        }
    }

Done 🙂

If you have any question or you’ve got a better approach in your mind just let me know.