Is there any fancy way to implement debounce
logic with Kotlin Android?
I'm not using Rx in project.
There is a way in Java, but it is too big as for me here.
Is there any fancy way to implement debounce
logic with Kotlin Android?
I'm not using Rx in project.
There is a way in Java, but it is too big as for me here.
You can use kotlin coroutines to achieve that. Here is an example.
Be aware that coroutines are experimental at kotlin 1.1+ and it may be changed in upcoming kotlin versions.
Since Kotlin 1.3 release, coroutines are now stable.
System.currentTimeMillis()
or similar? –
Kaohsiung 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? = null
fun searchDebounced(text: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(500)
search(text)
}
}
currentCoroutineContext().isActive
to check if the coroutine is still active or not. –
Intercom 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)
onTextChanged()
is not in the Android SDK, though this post contains an implementation that is compatible. –
Moitoso protected fun throttleClick(clickAction: (Unit) -> Unit) { viewModelScope.launch { throttleFirst(scope = this, action = clickAction) } }
but nothing happens, it just returns, the throttleFirst returning fun
doesn't fire. Why? –
Incurrent val invokeThrottledAction = throttleFirst(lifecycleScope, viewModel::doSomething); button.setOnClickListener { invokeThrottledAction() }
Basically you have to first create a function object and then call it whenever you need. –
Alphonsa launch
to get a scope I could just call val ViewModel.viewModelScope: CoroutineScope
since I'm already inside a ViewModel (if so, you are correct)? Btw, really helpful functions, thanks! –
Incurrent fun <T> debounce(...)
for example, it's the same with other functions. We call it once and it creates a function object of type (T) -> Unit
, which is kinda similar to to an object of anonymous Java class with one method. A Job reference is contained inside that object (kinda like inside a private field of an anonymous Java class). Each time something happens we use (call) the same function object. So, it doesn't matter how many times we call, it is the same object that has the same Job reference. I might be slightly wrong with the terms, but that's how I understand it. –
Alphonsa viewLifecyleOwner.lifecycleScope
comes from the androidx.lifecycle:lifecycle-runtime-ktx:2.2.0
. Here's more info: developer.android.com/topic/libraries/architecture/… –
Alphonsa throttleLatest
, in contrast to RxJava's, does not emit the first value immediately. This is more similar to throttleLast
, see the difference. BTW, debounce
and sample
(=throttleLast
) are already implemented in Kotlin Flows, but throttleLatest
is tricky to implement. –
Belicia throttleLatest
. And if your Flow is already conflated (e.g. StateFlow
), you can just delay
at the end of the collector. –
Belicia debounced
method is called from 2 different threads, they could result in creating 2 jobs and only one of them is eventually stored in var debounceJob
. The other job will keep running unless I'm missing something that will delete the job without a reference. Otherwise, I think a Channel
is needed to make these debounce calls sequential. –
Prevalent A more simple and generic solution is to use a function that returns a function that does the debounce logic, and store that in a val.
fun <T> debounce(delayMs: Long = 500L,
coroutineContext: CoroutineContext,
f: (T) -> Unit): (T) -> Unit {
var debounceJob: Job? = null
return { param: T ->
if (debounceJob?.isCompleted != false) {
debounceJob = CoroutineScope(coroutineContext).launch {
delay(delayMs)
f(param)
}
}
}
}
Now it can be used with:
val handleClickEventsDebounced = debounce<Unit>(500, coroutineContext) {
doStuff()
}
fun initViews() {
myButton.setOnClickListener { handleClickEventsDebounced(Unit) }
}
handleClickEventsDebounced(Unit)
Unit
is the param. It can be any type that you would like since we using generics. For a String for example do this: val handleClickEventsDebounced = debounce<String> = debounce<String>(500, coroutineContext) { doStuff(it) } Where 'it' is the string passed. Or name it with { myString -> doStuff(myString) } –
Ripe 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)
I have created a single extension function from the old answers of stack overflow:
fun View.clickWithDebounce(debounceTime: Long = 600L, action: () -> Unit) {
this.setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0
override fun onClick(v: View) {
if (SystemClock.elapsedRealtime() - lastClickTime < debounceTime) return
else action()
lastClickTime = SystemClock.elapsedRealtime()
}
})
}
View onClick using below code:
buttonShare.clickWithDebounce {
// Do anything you want
}
Thanks to https://medium.com/@pro100svitlo/edittext-debounce-with-kotlin-coroutines-fd134d54f4e9 and https://mcmap.net/q/266681/-throttle-onquerytextchange-in-searchview I wrote this code:
private var textChangedJob: Job? = null
private lateinit var textListener: TextWatcher
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
textListener = object : TextWatcher {
private var searchFor = "" // Or view.editText.text.toString()
override fun afterTextChanged(s: Editable?) {}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
val searchText = s.toString().trim()
if (searchText != searchFor) {
searchFor = searchText
textChangedJob?.cancel()
textChangedJob = launch(Dispatchers.Main) {
delay(500L)
if (searchText == searchFor) {
loadList(searchText)
}
}
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
editText.setText("")
loadList("")
}
override fun onResume() {
super.onResume()
editText.addTextChangedListener(textListener)
}
override fun onPause() {
editText.removeTextChangedListener(textListener)
super.onPause()
}
override fun onDestroy() {
textChangedJob?.cancel()
super.onDestroy()
}
I didn't include coroutineContext
here, so it probably won't work, if not set. For information see Migrate to Kotlin coroutines in Android with Kotlin 1.3.
textChangedJob?.cancel()
won't cancel a request in Retrofit. So, be ready that you will get all responses from all requests in random sequence. –
Beeck You can use kotlin coroutines to achieve that. Here is an example.
Be aware that coroutines are experimental at kotlin 1.1+ and it may be changed in upcoming kotlin versions.
Since Kotlin 1.3 release, coroutines are now stable.
System.currentTimeMillis()
or similar? –
Kaohsiung Using tags seems to be a more reliable way especially when working with RecyclerView.ViewHolder
views.
e.g.
fun View.debounceClick(debounceTime: Long = 1000L, action: () -> Unit) {
setOnClickListener {
when {
tag != null && (tag as Long) > System.currentTimeMillis() -> return@setOnClickListener
else -> {
tag = System.currentTimeMillis() + debounceTime
action()
}
}
}
}
Usage:
debounceClick {
// code block...
}
@masterwork,
Great answer. This is my implementation for a dynamic searchbar with an EditText. This provides great performance improvements so the search query is not performed immediately on user text input.
fun AppCompatEditText.textInputAsFlow() = callbackFlow {
val watcher: TextWatcher = doOnTextChanged { textInput: CharSequence?, _, _, _ ->
offer(textInput)
}
awaitClose { [email protected](watcher) }
}
searchEditText
.textInputAsFlow()
.map {
val searchBarIsEmpty: Boolean = it.isNullOrBlank()
searchIcon.isVisible = searchBarIsEmpty
clearTextIcon.isVisible = !searchBarIsEmpty
viewModel.isLoading.value = true
return@map it
}
.debounce(750) // delay to prevent searching immediately on every character input
.onEach {
viewModel.filterPodcastsAndEpisodes(it.toString())
viewModel.latestSearch.value = it.toString()
viewModel.activeSearch.value = !it.isNullOrBlank()
viewModel.isLoading.value = false
}
.launchIn(lifecycleScope)
}
@masterwork's answer worked perfectly fine. Here it is for ImageButton with compiler warnings removed:
@ExperimentalCoroutinesApi // This is still experimental API
fun ImageButton.onClicked() = callbackFlow<Unit> {
setOnClickListener { offer(Unit) }
awaitClose { setOnClickListener(null) }
}
// Listener for button
val someButton = someView.findViewById<ImageButton>(R.id.some_button)
someButton
.onClicked()
.debounce(500) // 500ms debounce time
.onEach {
clickAction()
}
.launchIn(lifecycleScope)
Tested. This works for me.
class DebounceJob {
private var job: Job? = null
fun debounce(
delayMs: Long = 500L,
scope: CoroutineScope,
func: () -> Unit,
): () -> Unit {
job?.cancel()
return {
job = scope.launch {
delay(delayMs)
func()
}
}
}
}
let debouceA = DebounceJob()
debouceA.debounce(2000, scope) {
anotherFunc()
}.invoke(Unit)
class
instead if you're planning to use it from multiple places. Doing so, job
will be localised to an instance, and not overall app/program. –
Phraseogram object ActionDelayer {
var isActionTriggered = false
fun delay(milliseconds: Long = 10000, scope: CoroutineScope = CoroutineScope(Dispatchers.IO), action: () -> Unit) {
if (!isActionTriggered) {
scope.launch {
action()
isActionTriggered = true
Executors.newSingleThreadScheduledExecutor().schedule({
isActionTriggered = false
}, milliseconds, TimeUnit.MILLISECONDS)
}
}
}
}
Usage:
ActionDelayer.delay { println("Repeating...") }
I've created a simple class to encapsulate the global variable and provide a convenience way to achieve debounce effect.
class Debouncer(
private val scope: CoroutineScope,
private val delay: Long,
factory: Debouncer.() -> DebounceAction,
) {
private var job: Job? = null
private var onAction: DebounceAction = factory()
fun startDebounce() {
job?.cancel()
job = scope.launch {
delay(delay)
onAction.run()
}
}
}
fun interface DebounceAction {
fun run()
}
Here i'm separating the startDebounce() to better control when the debounce should happen. The usage:
Debouncer(lifecycleScope, 3000) {
var count = 0
binding.btn.setOnClickListener {
count++
startDebounce()
}
// action on debounce
DebounceAction {
count = 0
}
}
With the extension, the usage is more natural:
fun CoroutineScope.debounce(
delay: Long,
factory: Debouncer.() -> DebounceAction,
): Debouncer {
return Debouncer(this, delay, factory)
}
lifecycleScope.debounce(3000) {
var count = 0
binding.btn.setOnClickListener {
count++
startDebounce()
}
// action on debounce
DebounceAction {
count = 0
}
}
You can also add more functionality, such debounce listener like this:
private val onDebounceListeners: MutableList<DebounceListener> = mutableListOf()
fun startDebounce() {
onDebounceListeners.onEach { it.onDebounce() }
...
...
}
fun addOnDebounceListener(block: () -> Unit): Debouncer {
onDebounceListeners.add(block)
return this
}
fun removeOnDebounceListener(listener: DebounceListener): Debouncer {
onDebounceListeners.remove(listener)
return this
}
fun debounceImmediate() = onAction
And then..
lifecycleScope.debounce(3000) {
var count = 0
binding.btn.setOnClickListener {
count++
startDebounce()
if (count == 10) {
// do something
..
..
// when you wanna trigger it manually
debounceImmediate()
}
}
// action on debounce
DebounceAction {
count = 0
}
}.addOnDebounceListener {
// do when debounce happen
}.addOnDebounceListener {
// do other thing
}
If anyone needs more easy kotlin extension here it is
fun EditText.debounce(delay: Long, action: (CharSequence?) -> Unit) {
doAfterTextChanged { text ->
val counter = getTag(id) as? Int ?: 0
handler.removeCallbacksAndMessages(counter)
handler.postDelayed(delay, ++counter) { action(text) }
setTag(id, counter)
}
}
Uses
val editText = findViewById<EditText>(R.id.editText)
editText.debounce(500) {
if (it.isNotEmpty()) {
// Submit the form
}
}
Based on what others have written (especially here), I've made a bit simpler solution:
class DebounceJob(private val @IntRange(from = 0L) debounceDelayMs: Long = 500L) {
private var job: Job? = null
@UiThread
fun debounce(delayMs: Long = debounceDelayMs, scope: CoroutineScope, runnable: Runnable) {
job?.cancel()
job = scope.launch {
delay(delayMs)
runnable.run()
}
}
}
Example usage in ViewModel:
private val debounceJob = DebounceJob()
...
debounceJob.debounce(scope = viewModelScope, runnable = {
viewModelScope.launch {
runInterruptible(Dispatchers.IO) {
//do something here in background thread
}
}
})
Full example usage here:
https://mcmap.net/q/266684/-how-to-retrieve-last-edited-contact-from-contactsprovider
If you want for LiveData, I've found this solution to be a nice one (from here):
fun <T> LiveData<T>.debounce(duration: Long = 1000L, coroutineScope: CoroutineScope): MediatorLiveData<T> {
val mediatorLiveData = MediatorLiveData<T>()
val source = this
var job: Job? = null
mediatorLiveData.addSource(source) {
job?.cancel()
job = coroutineScope.launch {
delay(duration)
mediatorLiveData.value = source.value
}
}
return mediatorLiveData
}
Usage:
liveData.debounce(500L, CoroutineScope(Dispatchers.Main))
.observe(viewLifecycleOwner) { ... }
This question is quite old and I think it deserves a new solution.
This solution using a StateFlow
. It can be done by a SharedFlow
. Both StateFlow
and StateFlow
has the following properties:
debounce
method which does exactly what it says. Quoting docs:
Returns a flow that mirrors the original flow, but filters out values that are followed by the newer values within the given timeout. The latest value is always emitted.
You can find the solution in this gist.
Here is a Debouncer
class that makes debouncing work easier:
class Debouncer(
coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Default),
private val waitMs: Long = 5000L
) {
class Work(val work: suspend () -> Unit)
private val myFlow = MutableStateFlow<Work?>(null)
init {
coroutineScope.launch {
myFlow.debounce(waitMs).collect {
it?.let {
it.work()
}
}
}
}
suspend fun debounce(work: suspend () -> Unit) {
myFlow.emit(Work(work))
}
}
Example of how to use it:
class Dog {
private val debouncer = Debouncer()
fun bark() {
debouncer.debounce {
// do the barking...
}
}
}
© 2022 - 2024 — McMap. All rights reserved.