Implement Instant Search Using Kotlin Flow Operators

Implement Instant Search Using Kotlin Flow Operators

In this blog, we are going to learn how to implement the instant search feature using Kotlin Flow operators in Android application. We will also learn about all the operators used to implement this feature.

Before starting, for your information, this blog post is a part of the series that we are writing on Flow APIs in Kotlin Coroutines.

Resources to get started with Kotlin Flow:

Let's get started.

I will be using this project for the implementation part. You can find the complete code for the implementation mentioned in this blog in the project itself.

The following are the things of Kotlin Flow that we will be using to implement this search feature:

  • StateFlow: We have already published an article on it. You can learn about it from here.
  • Debounce Operator
  • Filter Operator
  • DistinctUntilChanged Operator
  • FlatMapLatest Operator

Earlier this instant search feature implementation in Android was not that easy with Kotlin Coroutines, but now with Kotlin Flow Operators, it has become easy and interesting.

Let's get started

First of all, we will create the extension function to return the StateFlow so that we can apply our required operators to that.

So here, on the SearchView, we will use the setOnQueryTextListener to observe for the changes in the text, change the state of the query, and finally return the StateFlow like below:

fun SearchView.getQueryTextChangeStateFlow(): StateFlow<String> {

    val query = MutableStateFlow("")

    setOnQueryTextListener(object : SearchView.OnQueryTextListener {
        override fun onQueryTextSubmit(query: String?): Boolean {
            return true
        }

        override fun onQueryTextChange(newText: String): Boolean {
            query.value = newText
            return true
        }
    })

    return query

}

We have also simulated the data from the network with delay like below:

/**
 * Simulation of network data
 */
private fun dataFromNetwork(query: String): Flow<String> {
    return flow {
        delay(2000)
        emit(query)
    }
}

Now, on the QueryTextChangeStateFlow, we will apply the operators like below:

searchView.getQueryTextChangeStateFlow()
    .debounce(300)
    .filter { query ->
        if (query.isEmpty()) {
            textViewResult.text = ""
            return@filter false
        } else {
            return@filter true
        }
    }
    .distinctUntilChanged()
    .flatMapLatest { query ->
        dataFromNetwork(query)
            .catch {
                emitAll(flowOf(""))
            }
    }
    .flowOn(Dispatchers.Default)
    .collect { result ->
        textViewResult.text = result
    }

Now, it's time to learn why the above operators are used and how they work when they are put together.

Understanding Operators

  • Debounce: Here, the debounce operator is used with a time constant. The debounce operator handles the case when the user types “a”, “ab”, “abc”, in a very short time. So, there will be so many network calls. But the user is finally interested in the result of the search “abc”. So, we must discard the results of “a” and “ab”. Ideally, there should be no network calls for “a” and “ab” as the user typed those in a very short time. So, the debounce operator comes to the rescue. The debounce will wait for the provided time for doing anything, if any other search query comes in between that time, it will ignore the previous item and start waiting for that time again with the new search query. If nothing new comes in that given constant time, it will proceed with that search query for further processing. So, debounce only emit an item, if a particular timespan has passed without it emitting another item.
  • Filter: The filter operator is used to filter the unwanted string like an empty string in this case to avoid the unnecessary network call.
  • DistinctUntilChanged: The distinctUntilChanged operator is used to avoid duplicate network calls. Let say the last on-going search query was “abc” and the user deleted “c” and again typed “c”. So again it’s “abc”. So if the network call is already going on with the search query “abc”, it will not make the duplicate call again with the search query “abc”. So, distinctUntilChanged suppress duplicate consecutive items emitted by the source.
  • FlatMapLatest: Here, the flatMapLatest operator is used to avoid the network call results which are not needed more for displaying to the user. Let say the last search query was “ab” and there is an ongoing network call for “ab” and the user typed “abc”. Then, we are no more interested in the result of “ab”. We are only interested in the result of “abc”. So, the flatMapLatest comes to the rescue. It only provides the result for the last search query(most recent) and ignores the rest.

Note: If you notice inside the flatMapLatest, if we are getting any error, we are passing the empty result. We can change this based on our requirements.

This way, we are able to implement the instant search feature using Kotlin Flow Operators in an Android application.

You can find the end to end implementation in this project.

That's it for now.

Also, Let’s become friends on Twitter, Linkedin, Github, Quora, and Facebook.