Kotlin Android debounce
I've created a gist with three debounce operators inspired by this elegant solution from Patrick where I added two more similar cases: throttleFirst
and throttleLatest
. Both of these are very similar to their RxJava analogues (throttleFirst, throttleLatest).
throttleLatest
works similar to debounce
but it operates on time intervals and returns the latest data for each one, which allows you to get and process intermediate data if you need to.
fun <T> throttleLatest( intervalMs: Long = 300L, coroutineScope: CoroutineScope, destinationFunction: (T) -> Unit): (T) -> Unit { var throttleJob: Job? = null var latestParam: T return { param: T -> latestParam = param if (throttleJob?.isCompleted != false) { throttleJob = coroutineScope.launch { delay(intervalMs) latestParam.let(destinationFunction) } } }}
throttleFirst
is useful when you need to process the first call right away and then skip subsequent calls for some time to avoid undesired behavior (avoid starting two identical activities on Android, for example).
fun <T> throttleFirst( skipMs: Long = 300L, coroutineScope: CoroutineScope, destinationFunction: (T) -> Unit): (T) -> Unit { var throttleJob: Job? = null return { param: T -> if (throttleJob?.isCompleted != false) { throttleJob = coroutineScope.launch { destinationFunction(param) delay(skipMs) } } }}
debounce
helps to detect the state when no new data is submitted for some time, effectively allowing you to process a data when the input is completed.
fun <T> debounce( waitMs: Long = 300L, coroutineScope: CoroutineScope, destinationFunction: (T) -> Unit): (T) -> Unit { var debounceJob: Job? = null return { param: T -> debounceJob?.cancel() debounceJob = coroutineScope.launch { delay(waitMs) destinationFunction(param) } }}
All these operators can be used as follows:
val onEmailChange: (String) -> Unit = throttleLatest( 300L, viewLifecycleOwner.lifecycleScope, viewModel::onEmailChanged )emailView.onTextChanged(onEmailChange)
For a simple approach from inside a ViewModel
, you can just launch a job within the viewModelScope
, keep track of the job, and cancel it if a new value arises before the job is complete:
private var searchJob: Job? = nullfun searchDebounced(searchText: String) { searchJob?.cancel() searchJob = viewModelScope.launch { delay(500) search(searchText) }}
I use a callbackFlow and debounce from Kotlin Coroutines to achieve debouncing. For example, to achieve debouncing of a button click event, you do the following:
Create an extension method on Button
to produce a callbackFlow
:
fun Button.onClicked() = callbackFlow<Unit> { setOnClickListener { offer(Unit) } awaitClose { setOnClickListener(null) }}
Subscribe to the events within your life-cycle aware activity or fragment. The following snippet debounces click events every 250ms:
buttonFoo .onClicked() .debounce(250) .onEach { doSomethingRadical() } .launchIn(lifecycleScope)