⬆️ ⬇️

Implementing Instant Android Search with RxJava

Implementing Instant Android Search with RxJava



I am working on a new application, which, as is usually the case, communicates with the backend service to retrieve data through the API. In this example, I will develop a search function, one of the features of which will be an instant search right while typing.



Instant Search



Nothing complicated, you think. You just need to place the search component on the page (most likely, in the toolbar), connect the onTextChange event onTextChange and perform the 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 that's the problem. Since I need to implement a search directly during input, then whenever the onQueryTextChange() event handler is triggered, I call on the API to get the first result set. 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 just type my request, there are five API calls, each of which performs a search. For example, in the cloud you need to pay for each API call. Thus, when I enter my request, I need a short delay before sending it, so that only one call to the API will result.



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 


20 API calls occur! A small delay will reduce the number of these calls. I also want to get rid of duplicates so that the trimmed 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 typing characters or searching for a single character?



Reactive programming



There are several options for further action, but right now I want to focus on the technique, which is widely known as reactive programming and the RxJava library. When I first encountered 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 features of functional programming.

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



Delay Operator Chart



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



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



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


Remember to synchronize project dependencies to load the library.



Now let's consider a new solution. Using the old method, I accessed the API as I entered each new character. With the help of a new way I'm going to create an 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<String> { 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 in the use of the new technique is the presence of the jet stream - Observable . The text handler (or the request handler in this case) sends elements to the stream using the onNext() method. And Observable has subscribers who handle these items.



We can create a chain of methods before subscribing to reduce the list of strings to process. Let's start with the fact that the text sent will always be in lower case and there will be no spaces at the beginning and end of the line:



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


I shortened the methods to show the most significant part. Now the same logs look like this:



 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<String> { ... }) .map { text -> text.toLowerCase().trim() } .debounce(250, TimeUnit.MILLISECONDS) .subscribe { text -> Log.d(TAG, "subscriber: $text" } 


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



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


Note trans. In this case, it is more reasonable to use the distinctUntilChanged() operator, because otherwise, in the case of a repeated search on a string, the request is simply ignored. And when implementing such a search, it is reasonable to pay attention only to the last successful request and ignore the new one in case of its identity with the previous one.

Finally, we filter out empty queries:



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


At this stage, you will notice that only one (or maybe two) message is displayed in the logs, which means fewer API calls. At the same time, the application will continue to work adequately. Moreover, cases where you enter something but then delete and enter again will also result in fewer API calls.



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



 // Set up the query listener that executes the search Observable.create(ObservableOnSubscribe<String> { 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



Using this simple wrapper technique for text elements in Observable and 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 covered only a small part of the whole world of RxJava, so I leave to you links for additional reading on this topic:





')

Source: https://habr.com/ru/post/430394/



All Articles