Implementing instant search in Android using RxJava

Original author: Adrian Hall
  • Transfer

Implementing instant search in Android using RxJava


I am working on a new application that, as it usually happens, communicates with the backend service to receive data through the API. In this example, I will develop a search function, one of the features of which will be instant search right while entering text.


Instant search


Nothing complicated, you think. You just need to place the search component on the page (most likely in the toolbar), connect an event handler onTextChangeand perform a search. So here is what I did:


override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.menu_main, menu)
    val searchView = menu?.findItem(R.id.action_search)?.actionView as SearchView
    // Set up the query listener that executes the search
    searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String?): Boolean {
            Log.d(TAG, "onQueryTextSubmit: $query")
            return false
        }
        override fun onQueryTextChange(newText: String?): Boolean {
            Log.d(TAG, "onQueryTextChange: $newText")
            return false
        }
    })
    return super.onCreateOptionsMenu(menu)
}

But here is the problem. Since I need to implement a search right during input, whenever an event handler fires onQueryTextChange(), I turn to the API to get the first set of results. Logs are as follows:


D/MainActivity: onQueryTextChange: T
D/MainActivity: onQueryTextChange: TE
D/MainActivity: onQueryTextChange: TES
D/MainActivity: onQueryTextChange: TEST
D/MainActivity: onQueryTextSubmit: TEST

Despite the fact that I'm just printing my request, there are five API calls, each of which performs a search. For example, in the cloud, you need to pay for every call to the API. Thus, when I enter my request, I need a little delay before sending it, so that in the end only one call to the API takes place.


Now, suppose I want to find something else. I delete TEST and enter other characters:


D/MainActivity: onQueryTextChange: TES
D/MainActivity: onQueryTextChange: TE
D/MainActivity: onQueryTextChange: T
D/MainActivity: onQueryTextChange: 
D/MainActivity: onQueryTextChange: S
D/MainActivity: onQueryTextChange: SO
D/MainActivity: onQueryTextChange: SOM
D/MainActivity: onQueryTextChange: SOME
D/MainActivity: onQueryTextChange: SOMET
D/MainActivity: onQueryTextChange: SOMETH
D/MainActivity: onQueryTextChange: SOMETHI
D/MainActivity: onQueryTextChange: SOMETHIN
D/MainActivity: onQueryTextChange: SOMETHING
D/MainActivity: onQueryTextChange: SOMETHING 
D/MainActivity: onQueryTextChange: SOMETHING E
D/MainActivity: onQueryTextChange: SOMETHING EL
D/MainActivity: onQueryTextChange: SOMETHING ELS
D/MainActivity: onQueryTextChange: SOMETHING ELSE
D/MainActivity: onQueryTextChange: SOMETHING ELSE
D/MainActivity: onQueryTextSubmit: SOMETHING ELSE

There are 20 API calls! A small delay will reduce the number of these calls. I also want to get rid of duplicates so that the cropped text does not lead to repeated requests. I also probably want to filter out some elements. For example, do you need the ability to search without the entered characters or search by one character?


Reactive programming


There are several options for further action, but right now I want to focus on a technique that is commonly known as reactive programming and the RxJava library. When I first came across reactive programming, I saw the following description:


ReactiveX is an API that works with asynchronous structures and manipulates data streams or events using combinations of Observer and Iterator patterns, as well as functional programming features.

This definition does not fully explain the nature and strengths of ReactiveX. And if it explains, then only to those who are already familiar with the principles of this framework. I also saw charts like this:


Delay Operator Chart


The diagram explains the role of the operator, but does not fully understand the essence. So let's see if I can more clearly explain this diagram with a simple example.


Let's prepare our project first. You will need a new library in build.gradleyour application file :


implementation "io.reactivex.rxjava2:rxjava:2.1.14"

Remember to sync project dependencies to load the library.


Now let's look at a new solution. Using the old method, I accessed the API as I entered each new character. Using the new method, I'm going to create Observable:


override fun onCreateOptionsMenu(menu: Menu?): Boolean {
    menuInflater.inflate(R.menu.menu_main, menu)
    val searchView = menu?.findItem(R.id.action_search)?.actionView as SearchView
    // Set up the query listener that executes the search
    Observable.create(ObservableOnSubscribe { subscriber ->
        searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
            override fun onQueryTextChange(newText: String?): Boolean {
                subscriber.onNext(newText!!)
                return false
            }
            override fun onQueryTextSubmit(query: String?): Boolean {
                subscriber.onNext(query!!)
                return false
            }
        })
    })
    .subscribe { text ->
        Log.d(TAG, "subscriber: $text")
    }
    return super.onCreateOptionsMenu(menu)
}

This code does exactly the same thing as the old code. Logs are as follows:


D/MainActivity: subscriber: T
D/MainActivity: subscriber: TE
D/MainActivity: subscriber: TES
D/MainActivity: subscriber: TEST
D/MainActivity: subscriber: TEST

However, the key difference between using the new technique is the presence of a reactive stream - Observable. The text handler (or request handler in this case) sends the elements to the stream using the method onNext(). And Observablethere are subscribers who process these elements.


We can create a chain of methods before subscribing to reduce the list of strings for processing. To begin with, the sent text will always be lowercase and there will be no spaces at the beginning and end of the line:


Observable.create(ObservableOnSubscribe { ... })
.map { text -> text.toLowerCase().trim() }
.subscribe { text -> Log.d(TAG, "subscriber: $text" }

I cut methods to show the most significant part. Now the same logs are as follows:


D/MainActivity: subscriber: t
D/MainActivity: subscriber: te
D/MainActivity: subscriber: tes
D/MainActivity: subscriber: test
D/MainActivity: subscriber: test

Now let's apply a delay of 250ms, expecting more content:


Observable.create(ObservableOnSubscribe { ... })
.map { text -> text.toLowerCase().trim() }
.debounce(250, TimeUnit.MILLISECONDS)
.subscribe { text -> Log.d(TAG, "subscriber: $text" }

And finally, remove duplicate stream so that only the first unique request is processed. Subsequent identical requests will be ignored:


Observable.create(ObservableOnSubscribe { ... })
.map { text -> text.toLowerCase().trim() }
.debounce(100, TimeUnit.MILLISECONDS)
.distinct()
.subscribe { text -> Log.d(TAG, "subscriber: $text" }

Note perev. In this case, it is more reasonable to use the operator distinctUntilChanged(), because otherwise, in the case of repeated search on any line, the query will simply be ignored. And when implementing such a search, it is reasonable to pay attention only to the last successful request and ignore the new one if it is identical with the previous one.

At the end, we filter out empty queries:


Observable.create(ObservableOnSubscribe { ... })
.map { text -> text.toLowerCase().trim() }
.debounce(100, TimeUnit.MILLISECONDS)
.distinct()
.filter { text -> text.isNotBlank() }
.subscribe { text -> Log.d(TAG, "subscriber: $text" }

At this point, you will notice that only one (or maybe two) message is displayed in the logs, which indicates fewer API calls. In this case, the application will continue to work adequately. Moreover, cases when you enter something, but then delete and enter again, will also lead to fewer calls to the API.


There are many more different operators that you can add to this pipeline, depending on your goals. I believe that they are very useful for working with input fields that interact with the API. The full code is as follows:


// Set up the query listener that executes the search
Observable.create(ObservableOnSubscribe { subscriber ->
    searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextChange(newText: String?): Boolean {
            subscriber.onNext(newText!!)
            return false
        }
        override fun onQueryTextSubmit(query: String?): Boolean {
            subscriber.onNext(query!!)
            return false
        }
    })
})
.map { text -> text.toLowerCase().trim() }
.debounce(250, TimeUnit.MILLISECONDS)
.distinct()
.filter { text -> text.isNotBlank() }
.subscribe { text ->
    Log.d(TAG, "subscriber: $text")
}

Now I can replace the log message with a call to the ViewModel to initiate an API call. However, this is a topic for another article.


Conclusion


With this simple technique of wrapping text elements in Observableand using RxJava, you can reduce the number of API calls that are needed to perform server operations, as well as improve the responsiveness of your application. In this article, we have covered only a small part of the whole world of RxJava, so I leave you links for additional reading on this topic:


  • Dan Lew Grokking RxJava (this is the site that helped me move in the right direction).
  • ReactiveX site (I often refer to this site when building pipeline).

Also popular now: