https://kotlinlang.org logo
Title
p

Pablo

05/18/2021, 3:38 PM
Is there any way to "throat" or "debounce" when user click on a button (in Android) to avoid multi-clicks? I've found something like this
fun <T> debounce(
    delayMillis: Long = 300L,
    scope: CoroutineScope,
    action: (T) -> Unit
): (T) -> Unit {
    var debounceJob: Job? = null
    return { param: T ->
        if (debounceJob == null) {
            debounceJob = scope.launch {
                action(param)
                delay(delayMillis)
                debounceJob = null
            }
        }
    }
}
Where I can do
fun setDebounceListener(view: Button, onClickListener: View.OnClickListener) {
    val clickWithDebounce: (view: View) -> Unit =
        debounce(scope = MainScope()) {
            onClickListener.onClick(it)
        }
    view.setOnClickListener(clickWithDebounce)
}
Does it make sense to use debounce here? It's just to avoid double click on a Button
l

louiscad

05/18/2021, 4:20 PM
You can also disable the button on click and re-enable it only at the right time.
See this function from Splitties View Coroutines: https://github.com/LouisCAD/Splitties/blob/1936c6c7e445c048077d8b4b7bb1c13b99e0ca0[…]idMain/kotlin/splitties/views/coroutines/VisibilityAndClicks.kt If you every need to turn it into a flow, it's pretty easy as well, you just need an infinite loop that calls
awaitOneClick()
in the
flow { … }
builder.
p

Pablo

05/19/2021, 7:35 AM
How do I cancel it?
I've tried your code and it says no value passed for onCancellation
l

louiscad

05/19/2021, 7:36 AM
It's a suspending function, you cancel it like another one.
p

Pablo

05/19/2021, 7:40 AM
Then do I have to create a coroutineScope to launch it, right?
l

louiscad

05/19/2021, 7:40 AM
Yep, or use the
xxxLatest
operators if called from a flow operator
p

Pablo

05/19/2021, 7:40 AM
But I mean, I have this continuation.resume(Unit) and it's complaining because I have to set a new parameter for onCancellation
How would be the debouncing with flow? Because AFAIK there's a debounce method in flow, right?
l

louiscad

05/19/2021, 7:42 AM
You'd not debounce, you'd simply not call
awaitOneClick()
again until your app wants to allow clicks again
Look at the imports in that file, that'll help you fix the complaint of the file.
Example usage:
suspend handleStuff() {
    while (true) {
        btn.awaitOneClick()
        doStuff()
    }
}
No need for magic timers 🙌🏼
p

Pablo

05/19/2021, 7:44 AM
sorry I copied the wrong thing
l

louiscad

05/19/2021, 7:44 AM
(Given
doStuff()
is a suspending function)
p

Pablo

05/19/2021, 7:44 AM
fun View.clicks(): Flow<Unit> = callbackFlow {
    setOnClickListener {
        offer(Unit)
    }
    awaitClose { setOnClickListener(null) }
   }
l

louiscad

05/19/2021, 7:45 AM
Then delete the message 😉
p

Pablo

05/19/2021, 7:45 AM
button.clicks().debounce(1000).onEach { println("clicked") }.launchIn(GlobalScope)
l

louiscad

05/19/2021, 7:46 AM
The correct way to transform it to a
Flow
is the following:
fun View.clicks(): Flow<Unit> = flow {
    while (true) {
        awaitOneClick()
        emit(Unit)
    }
}
It handles backpressure, so the view will stay disabled while
emit
is suspending (which is when the lambda of
collect
is executing)
p

Pablo

05/19/2021, 7:48 AM
Ah but this is mixing your solution with flow, right?
What's the difference between the code I put and using your solution?
l

louiscad

05/19/2021, 7:49 AM
Mine disables the view (e.g. button) when the app is busy and not ready to accept new requests from the user
p

Pablo

05/19/2021, 7:49 AM
ah okok
l

louiscad

05/19/2021, 7:49 AM
It all boils down to the implementation of
awaitOneClick()
p

Pablo

05/19/2021, 7:49 AM
Let me try then
And the way to call this function from the view?
l

louiscad

05/19/2021, 7:50 AM
You don't call it from the View istelf. You call it from the code that controls the View(s)
Can be a custom class, or even a suspending function that can be top-level
p

Pablo

05/19/2021, 7:50 AM
The thing is that I have problems from a RecyclerView adapter (View Holder)
So I'd need to control the onClickListener there
p

Pablo

05/19/2021, 7:54 AM
Ah ok
Uh, guess it's overkill for an easy think I want
Do all this stuff just to avoid double clicks on a button
l

louiscad

05/19/2021, 7:56 AM
I don't think it's that overkill personally, but I might be biased 😉
p

Pablo

05/19/2021, 7:58 AM
It's bad praxis if I pass the CoroutineScope to adapter and then launch{button.onSingleClick()}?
l

louiscad

05/19/2021, 7:59 AM
bad practice? I don't think so, you just need to make sure the scope is from the view lifecycle or a sub-scope
I'm not convinced by the naming of your onSingleClick function though, it looks like a callback.
p

Pablo

05/19/2021, 8:01 AM
yeye sorry bad naming
So the issue now is, in continuation.resume(Unit)
is waiting for a cancellation callback but you do not have that, why?
l

louiscad

05/19/2021, 8:01 AM
I already told you to look at the imports in my original snippet
You're missing one import
There's an extension function. Did you find it?
p

Pablo

05/19/2021, 8:02 AM
import kotlin.coroutines.resume
:yes: 1
So I have to pass as a parameter a lambda? to do my Unit thing when onClickListener, right?
imagine I had button.setOnClickListener { itemClicked.invoke(position) }
I did it like :
scope.launch {
  singleClick().onEach {
    itemClicked.invoke(position)
  }.launchIn(this)
}
it's redundant I guess I'm already launching it from scope, so no need to add launchIn right? 😕
l

louiscad

05/19/2021, 8:28 AM
You likely want to use
collect
instead, yup. Also, having a flow that actually gives only one value ever is weird, name-wise, and use-wise, just using
awaitOneClick()
would make more sense here.
p

Pablo

05/19/2021, 8:30 AM
true
If I use awaitOneClick() can I pass a lambda then? Like I wrote before?
Also what I've found is: From recyclerView click > it goes to detail (can not do multi click) but when going back to the list I can not click anymore, and also I'm doing this to do the click and not sure if it's ok scope.launch{awaitOneClick() onItemClicked.invoke()}
l

louiscad

05/19/2021, 8:46 AM
That's why I was using a loop in my example.
p

Pablo

05/19/2021, 8:47 AM
This you mean?
fun View.clicks(): Flow<Unit> = flow {
    while (true) {
        awaitOneClick()
        emit(Unit)
    }
}
But then I got your point to not use flow for 1 emit and 1 collect
p

Pablo

05/19/2021, 8:47 AM
And I want to use just the awaitOneClick() with a lambda (it's an unit)
I don't get why I have to do a while(true) there 😕
l

louiscad

05/19/2021, 8:50 AM
Because the function handles only one click ever
So you need to call it again
But if that doesn't suit your use case, don't use it 🤷🏼
p

Pablo

05/19/2021, 8:52 AM
Well I just wanted to try to avoid double click on a button (passing a lambda or something) do the action just once and that's it, then if I tap the button again, work the same, but I see that in your example don't check this "timing" stuff so that's why it's not working
the problem is that my button can do a multiple click on a item and it starts 2 activities at least (the same one)
So I wanted to avoid that, and since I'm starting with coroutines/flow was looking for a cool solution of doing this
l

louiscad

05/19/2021, 8:56 AM
If you are starting an activity, you probably want to call
delay(timeMillis = 700)
or alike after doing it before calling
awaitOneClick()
again
p

Pablo

05/19/2021, 8:57 AM
Sure but I wanted to encapsulate this everything to an extensionFunction so I don't have to do the while(true) delay() etc on every button
l

louiscad

05/19/2021, 8:58 AM
You can if you want.
p

Pablo

05/19/2021, 8:58 AM
Neither create a SuspendButton View and change all the buttons at least from now
Creating an extension function of coroutineScope?
Or in the same extensionFunction? (awaitOneClick)
l

louiscad

05/19/2021, 9:00 AM
Probably more an extension on
View
that takes a
CoroutineScope
p

Pablo

05/19/2021, 9:01 AM
And then the while true and everything inside the suspendcancelable?
l

louiscad

05/19/2021, 9:01 AM
No
That extension would use
awaitOneClick()
.
suspendCancellable
is an implementation detail of
awaitOneClick
p

Pablo

05/19/2021, 9:03 AM
From now I have this
suspend fun View.awaitOneClick(
    disableAfterClick: Boolean = true,
    hideAfterClick: Boolean = false,
    onClick: (View) -> Unit,
) = try {
    if (disableAfterClick) isEnabled = true
    if (hideAfterClick) isVisible = true
    suspendCancellableCoroutine<Unit> { continuation ->
        setOnClickListener {
            onClick.invoke(it)
            continuation.resume(Unit)
        }
    }
} finally {
    setOnClickListener(null)
    if (disableAfterClick) isEnabled = false
    if (hideAfterClick) isVisible = false
}
l

louiscad

05/19/2021, 9:03 AM
Yes, I know that code, I wrote it 😛
p

Pablo

05/19/2021, 9:04 AM
I've pased the lambda so I can call this function as
awaitOneClick { //do something }
l

louiscad

05/19/2021, 9:04 AM
You can leave it like that, and use it intor your own function that takes whatever lambda you want
And, please, with a different name, I trademarked
awaitOneClick()
(kidding about the TM, but you can find a clearer name to the function that'd use
awaitOneClick()
)
p

Pablo

05/19/2021, 9:06 AM
And now if I pass coroutineScope via parameter, then I don't know where to but what I was doing in the ViewHolder
scope.launch{
  while(true){
    delay(700)
    awaitOnClick{
       onClick.invoke(position)
    }
}
l

louiscad

05/19/2021, 9:09 AM
You see the code you want to pass in the non existing lambda of
awaitOneClick()
? Just put if after calling
awaitOneClick()
.
Also, the
delay
should be after that code, not before.
Otherwise, you're just slowing your app down and annoying all your users.
I spent too much time on this now, I hope I was helpful 🙂
p

Pablo

05/19/2021, 9:10 AM
So to add it in the extensionFunction shall I create a new one? And then use yours? it's unclear where to put this while(true) How would be the pseudo? Not the real code...
You see the code you want to pass in the non existing lambda of 
awaitOneClick()
?
I add it in your code when I said "I have this from now"
1
l

louiscad

05/19/2021, 9:13 AM
Oh, sorry, you should not edit that and use the one I linked
I didn't notice that onClick extra parameter.
into an extension function
launchLoop
is
launch
on a scope +
while (isActive)
p

Pablo

05/19/2021, 9:21 AM
Cool, I think I got it, ignoring the naming, this should be like a good option?
fun View.handleSingleClick(scope: CoroutineScope, block: () -> Unit) {
scope.launch{
  while(true){
   awaitOneClick()
   block()
   delay(700)
 } 
}
}
l

louiscad

05/19/2021, 9:26 AM
Ignoring the naming, yes.
p

Pablo

05/19/2021, 9:26 AM
any difference my while(true) with your launchLoop extension? Mine could cause leaks?
l

louiscad

05/19/2021, 9:27 AM
They are the same, but if you want to be sure there's zero leaks, you need the right scope, AND to use suspend-bind that I linked before from another conversation.
1
That's why I was suggesting to use that for RecyclerView from the beginning 🙂
1