Implementing Search-on-type in Android with Coroutines
By: Phạm Giang
Tags: Search, Kotlin Coroutines, Android, Debouncing, Searchview

We have to agree, search-on-type has been a common feature in Android applications. The functionality requirement is pretty straight forward: the app starts searching as the user starts typing. However, the technical requirement is not so simple. The app should not search immediately when the text changes, but only after an interval. This saves more than 50% of requests if the Android client is fetching data from an API.

The behaviour of the search view should be the same as debounce operator in ReactiveX

To achieve this, if a developer is using RxJava inside his/her project, he/she will be likely to use RxAndroid and RxBinding, and write a few easy lines of code. For example:

Just easy as that.

But this is the case of RxJava projects. What about projects use another concurrency solutions such as Coroutines?

In this piece of writing, I wouldn’t explain what is Coroutines or how to use it. I will only focus on how to make a debouncing search view with Coroutines.

First attempt

Here, I create an OnQueryTextListener implementation, which has a CoroutineScope with Main dispatcher and a Job (searchJob) variable. Whenever the text changes, the previous search is cancelled if it hasn’t been dispatched for more than a second, and a new search is launched.

Looks nice, isn’t it? But we can do it better. The code above is not reusable, which is not handy if the project has many search views. Trust me, there are applications with more than 3 search views in it.

Second attempt

What we could do is to write a class that implements OnQueryTextListener , which requires a lambda as a property. That lambda acts like a callback and is invoked after the delay call (line 19). This way, we make this Listener reusable and bring some clean code guidelines into the game, which is just awesome! Moreover, the debounce period is now adjustable by making it a property with public setter (line 4). I am not sure if making it fixed in initialisation time or allowing it to be changed in runtime is better. For now, this doesn’t break anything. The code in the Activity/Fragment will become:

But wait, adjustable debounce period is not so safe. Imagine if a guy sets it to 3 minutes (I know this is unrealistic, but it can happen theoretically), and the user probably doesn’t stay in that screen (Activity/Fragment) that long, why would we still dispatch the search? Let save some resource here.

How does the Listener even know if the Activity or Fragment is being destroyed? Luckily, Lifecycle-aware components are there to save the day.

Final attempt!!!

Making the listener a LifecycleObserver , passing in a Lifecycle and observe it at initialisation time. Whenever the lifecycle is destroyed, perform a cancellation (Line 33). Those are all the thing added above.

And here is how to use it.

I am not saying that this is the most effective way, at least, an extra dependency is required, and maybe this is a bit over-engineering. At the time writing this article, I use ViewModel and LiveData in my project, which comes with the Lifecycle stuff, that’s why I come up with the idea.

You can find the project here. Thanks for reading!

UPDATE

In Google I/O’ 19, LifecycleCoroutineScope is introduced. This even allows me to write even shorter DebouncingQueryTextListener. For example:

internal class DebouncingQueryTextListener(

lifecycle: Lifecycle,

private val onDebouncingQueryTextChange: (String?) -> Unit

) : SearchView.OnQueryTextListener {

var debouncePeriod: Long = 500



private val coroutineScope = lifecycle.coroutineScope



private var searchJob: Job? = null



override fun onQueryTextSubmit(query: String?): Boolean {

return false

}



override fun onQueryTextChange(newText: String?): Boolean {

searchJob?.cancel()

searchJob = coroutineScope.launch {

newText?.let {

delay(debouncePeriod)

onDebouncingQueryTextChange(newText)

}

}

return false

}

}