Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I debounce a setOnClickListener for 1 second using Kotlin Coroutines?

When user taps fast on the button the showDialog() method displays multiple times on top of each other, so when you dismiss it there is another one behind it. I am looking for a way to ignore the second tap for 1 second without using a handler or check the previous tap's time.

//Button that opens a dialog
button.setOnClickListener {
    showDialog()
}

I am looking for a solution using Kotlin coroutines or Kotlin flows for future implementations.

like image 442
Yannis Barmpakantze Avatar asked Feb 12 '20 14:02

Yannis Barmpakantze


People also ask

What is debounce Kotlin?

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? =

What is runBlocking in coroutines?

Definition of runBlocking() functionRuns a new coroutine and blocks the current thread interruptible until its completion. This function should not be used from a coroutine. It is designed to bridge regular blocking code to libraries that are written in suspending style, to be used in main functions and in tests.

Do coroutines run main thread?

Once the withContext block finishes, the coroutine in login() resumes execution on the main thread with the result of the network request.


2 Answers

Couroutines are overkill for something as trivial as debounce:

class DebounceOnClickListener(
    private val interval: Long,
    private val listenerBlock: (View) -> Unit
): View.OnClickListener {
    private var lastClickTime = 0L

    override fun onClick(v: View) {
        val time = System.currentTimeMillis()
        if (time - lastClickTime >= interval) {
            lastClickTime = time
            listenerBlock(v)
        }
    }
}

fun View.setOnClickListener(debounceInterval: Long, listenerBlock: (View) -> Unit) =
    setOnClickListener(DebounceOnClickListener(debounceInterval, listenerBlock))

Usage:

myButton.setOnClickListener(1000L) { doSomething() }
like image 161
Tenfour04 Avatar answered Sep 19 '22 10:09

Tenfour04


It's better to use a simple Flag for that instead of delay as it's not a good user experience.

But if you want to use Coroutines, You can simply use Kotlin Coroutine's Flow to apply this:

First I created an Extension Function for the click event that returns a Coroutine's Flow. like this:

    fun View.clicks(): Flow<Unit> = callbackFlow {
    setOnClickListener {
        offer(Unit)
    }
    awaitClose { setOnClickListener(null) }
   } 

Now, All you need is Calling your Function in onCreate like this:

button.clicks().debounce(1000).onEach { println("clicked") }.launchIn(GlobalScope)

Don't forget to add these lines in build.gradle file:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.3'

Edit:

The Flow analogue of throttleFirst operator is not implemented yet in kotlin coroutines. however, can be implemented with the help of Extension Functions:

@FlowPreview
@ExperimentalCoroutinesApi
fun <T> Flow<T>.throttleFirst(windowDuration: Long): Flow<T> = flow {
    var lastEmissionTime = 0L
    collect { upstream ->
        val currentTime = System.currentTimeMillis()
        val mayEmit = currentTime - lastEmissionTime > windowDuration
        if (mayEmit)
        {
            lastEmissionTime = currentTime
            emit(upstream)
        }
    }
}

The changes are as follows:

binding.button.clicks().throttleFirst(1250)
        .onEach {
            //delay(100)
            showDialog()
        }.launchIn(GlobalScope)

Also, you can use a delay() to handle this. Take it easy to change value of these parameters according to your needs.

like image 39
Morteza Nedaei Avatar answered Sep 19 '22 10:09

Morteza Nedaei