105

I have a problem with my app that if the user clicks the button multiple times quickly, then multiple events are generated before even my dialog holding the button disappears

I know a solution by setting a boolean variable as a flag when a button is clicked so future clicks can be prevented until the dialog is closed. However I have many buttons and having to do this everytime for every buttons seems to be an overkill. Is there no other way in android (or maybe some smarter solution) to allow only only event action generated per button click?

What's even worse is that multiple quick clicks seems to generate multiple event action before even the first action is handled so if I want to disable the button in the first click handling method, there are already existing events actions in the queue waiting to be handled!

Please help Thanks

Snake
  • 14,228
  • 27
  • 117
  • 250
  • can we have implemented code used GreyBeardedGeek code. Im not sure how you did use abstract class in ur Activity? – CoDe Apr 18 '15 at 07:16
  • Try this solution as well https://stackoverflow.com/questions/20971484/how-to-avoid-multiple-button-click-at-same-time-in-android – NoWar Apr 04 '18 at 05:16
  • I solved a similar problem [using backpressure in RxJava](https://stackoverflow.com/q/52966919/1916449) – arekolek Oct 24 '18 at 12:40

24 Answers24

123

Here's a 'debounced' onClick listener that I wrote recently. You tell it what the minimum acceptable number of milliseconds between clicks is. Implement your logic in onDebouncedClick instead of onClick

import android.os.SystemClock;
import android.view.View;

import java.util.Map;
import java.util.WeakHashMap;

/**
 * A Debounced OnClickListener
 * Rejects clicks that are too close together in time.
 * This class is safe to use as an OnClickListener for multiple views, and will debounce each one separately.
 */
public abstract class DebouncedOnClickListener implements View.OnClickListener {

    private final long minimumIntervalMillis;
    private Map<View, Long> lastClickMap;

    /**
     * Implement this in your subclass instead of onClick
     * @param v The view that was clicked
     */
    public abstract void onDebouncedClick(View v);

    /**
     * The one and only constructor
     * @param minimumIntervalMillis The minimum allowed time between clicks - any click sooner than this after a previous click will be rejected
     */
    public DebouncedOnClickListener(long minimumIntervalMillis) {
        this.minimumIntervalMillis = minimumIntervalMillis;
        this.lastClickMap = new WeakHashMap<>();
    }

    @Override 
    public void onClick(View clickedView) {
        Long previousClickTimestamp = lastClickMap.get(clickedView);
        long currentTimestamp = SystemClock.uptimeMillis();

        lastClickMap.put(clickedView, currentTimestamp);
        if(previousClickTimestamp == null || Math.abs(currentTimestamp - previousClickTimestamp) > minimumIntervalMillis) {
            onDebouncedClick(clickedView);
        }
    }
}
Johnny Five
  • 987
  • 1
  • 14
  • 29
GreyBeardedGeek
  • 29,460
  • 2
  • 47
  • 67
  • 4
    Oh if only every website could be bothered implementing something along these lines! No more "don't click submit twice or you will be charged twice"! Thank you. – Floris May 14 '13 at 03:42
  • Thank you,however I have a question. could it be that the onClick method in your code above is being excuted twice simultanously. So in that case some race condition happens – Snake May 14 '13 at 13:03
  • 2
    As far as I know, no. The onClick should always happen on the UI thread (there is only one). So multiple clicks, while close in time, should always be sequential, on the same thread. – GreyBeardedGeek May 14 '13 at 13:58
  • Use a single statement instead of this mess with this project.: https://github.com/fengdai/clickguard. – Feng Dai Feb 11 '15 at 03:54
  • 52
    @FengDai Interesting - this "mess" does what your library does, in about 1/20th of the amount of code. You have an interesting alternative solution, but this is isn't the place to insult working code. – GreyBeardedGeek Feb 11 '15 at 20:19
  • 15
    @GreyBeardedGeek Sorry for using that bad word. – Feng Dai Mar 31 '15 at 11:05
  • 1
    What in case if you have multiple button on screen, since even button.Click work on UI thread which mean sequential click, but still user can have enough time to perform simultaneous click on two different button...any suggestion? – CoDe Apr 18 '15 at 07:20
  • @GreyBeardedGeek thanks for your solution, it works well except for `ToggleButton` with state list and `textOn`, `textOff`. When I click toggle button many times quickly, the method `onDebouncedClick` called one time, it works right. But its state still change many times. I'm really confused. Would you please give me some advise? Thanks – Weiyi Mar 10 '16 at 08:31
  • 3
    This is throttling, not debouncing. – ancyrweb Feb 05 '19 at 10:53
77

With RxBinding it can be done easily. Here is an example:

RxView.clicks(view).throttleFirst(500, TimeUnit.MILLISECONDS).subscribe(empty -> {
            // action on click
        });

Add the following line in build.gradle to add RxBinding dependency:

compile 'com.jakewharton.rxbinding:rxbinding:0.3.0'
Nikita Barishok
  • 1,312
  • 1
  • 12
  • 15
  • 3
    Best decision. Just note that this holds strong link to your view so fragment won't be collected after usual lifecycle is done - wrap it into Trello's Lifecycle library. Then it will be unsubscribed and view freed after chosen lifecycle method is called (usually onDestroyView) – Den Drobiazko Mar 25 '16 at 14:19
  • 2
    This doesn't work with adapters, still can click on multiple items, and it will do the throttleFirst but on each individual views. – desgraci Oct 06 '16 at 19:35
  • 1
    Can be implemented with Handler easily, no need to use RxJava here. – Miha_x64 Sep 01 '18 at 08:30
  • This is a good option, as long as you're happy putting in all the associated Rx boilerplate (e.g. CompositeDisposables) into your code. It looks simple enough here, but doing this alone wouldn't be a complete solution. – Elroid Jul 09 '21 at 14:49
30

We can do it without any library. Just create one single extension function:

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
}
Machavity
  • 30,841
  • 27
  • 92
  • 100
SANAT
  • 8,489
  • 55
  • 66
  • 2
    This is cool! Though I think the more appropriate term here is "throttle" not "debounce". i.e. clickWithThrottle() – hopia Aug 10 '19 at 19:44
  • 1
    `private var lastClickTime: Long = 0` should be moved out of setOnClickListener{...} or the lastClickTime will always be 0. – Robert Nov 27 '20 at 03:05
  • 3
    @Robert Nope. lastClickTime can be inside OnClickListener, because only one instance of OnClickListener is created and it keeps its state. Then when onClick is called it has an access to the same variable. – Andrew Aug 30 '22 at 11:25
  • 1
    Clean solution. No need to add another library to the project and clean enough to be implemented by yourself. Though I agree with @hopia, throttle name is more suitable here. – Andrew Aug 30 '22 at 11:36
24

Here's my version of the accepted answer. It is very similar, but doesn't try to store Views in a Map which I don't think is such a good idea. It also adds a wrap method that could be useful in many situations.

/**
 * Implementation of {@link OnClickListener} that ignores subsequent clicks that happen too quickly after the first one.<br/>
 * To use this class, implement {@link #onSingleClick(View)} instead of {@link OnClickListener#onClick(View)}.
 */
public abstract class OnSingleClickListener implements OnClickListener {
    private static final String TAG = OnSingleClickListener.class.getSimpleName();

    private static final long MIN_DELAY_MS = 500;

    private long mLastClickTime;

    @Override
    public final void onClick(View v) {
        long lastClickTime = mLastClickTime;
        long now = System.currentTimeMillis();
        mLastClickTime = now;
        if (now - lastClickTime < MIN_DELAY_MS) {
            // Too fast: ignore
            if (Config.LOGD) Log.d(TAG, "onClick Clicked too quickly: ignored");
        } else {
            // Register the click
            onSingleClick(v);
        }
    }

    /**
     * Called when a view has been clicked.
     * 
     * @param v The view that was clicked.
     */
    public abstract void onSingleClick(View v);

    /**
     * Wraps an {@link OnClickListener} into an {@link OnSingleClickListener}.<br/>
     * The argument's {@link OnClickListener#onClick(View)} method will be called when a single click is registered.
     * 
     * @param onClickListener The listener to wrap.
     * @return the wrapped listener.
     */
    public static OnClickListener wrap(final OnClickListener onClickListener) {
        return new OnSingleClickListener() {
            @Override
            public void onSingleClick(View v) {
                onClickListener.onClick(v);
            }
        };
    }
}
BoD
  • 10,838
  • 6
  • 63
  • 59
9

UPDATE: This library is not recommended any more. I prefer Nikita's solution. Use RxBinding instead.

You can use this project: https://github.com/fengdai/clickguard to resolve this problem with a single statement:

ClickGuard.guard(button);
Feng Dai
  • 633
  • 5
  • 9
8

Here's a simple example:

public abstract class SingleClickListener implements View.OnClickListener {
    private static final long THRESHOLD_MILLIS = 1000L;
    private long lastClickMillis;

    @Override public void onClick(View v) {
        long now = SystemClock.elapsedRealtime();
        if (now - lastClickMillis > THRESHOLD_MILLIS) {
            onClicked(v);
        }
        lastClickMillis = now;
    }

    public abstract void onClicked(View v);
}
Christopher Perry
  • 38,891
  • 43
  • 145
  • 187
5

Just a quick update on GreyBeardedGeek solution. Change if clause and add Math.abs function. Set it like this:

  if(previousClickTimestamp == null || (Math.abs(currentTimestamp - previousClickTimestamp.longValue()) > minimumInterval)) {
        onDebouncedClick(clickedView);
    }

The user can change the time on Android device and put it in past, so without this it could lead to bug.

PS: don't have enough points to comment on your solution, so I just put another answer.

NixSam
  • 615
  • 6
  • 20
5

Here's something that will work with any event, not just clicks. It will also deliver the last event even if it's part of a series of rapid events (like rx debounce).

class Debouncer(timeout: Long, unit: TimeUnit, fn: () -> Unit) {

    private val timeoutMillis = unit.toMillis(timeout)

    private var lastSpamMillis = 0L

    private val handler = Handler()

    private val runnable = Runnable {
        fn()
    }

    fun spam() {
        if (SystemClock.uptimeMillis() - lastSpamMillis < timeoutMillis) {
            handler.removeCallbacks(runnable)
        }
        handler.postDelayed(runnable, timeoutMillis)
        lastSpamMillis = SystemClock.uptimeMillis()
    }
}


// example
view.addOnClickListener.setOnClickListener(object: View.OnClickListener {
    val debouncer = Debouncer(1, TimeUnit.SECONDS, {
        showSomething()
    })

    override fun onClick(v: View?) {
        debouncer.spam()
    }
})

1) Construct Debouncer in a field of the listener but outside of the callback function, configured with timeout and the callback fn that you want to throttle.

2) Call your Debouncer's spam method in the listener's callback function.

vlazzle
  • 811
  • 9
  • 14
5

So, this answer is provided by ButterKnife library.

package butterknife.internal;

import android.view.View;

/**
 * A {@linkplain View.OnClickListener click listener} that debounces multiple clicks posted in the
 * same frame. A click on one button disables all buttons for that frame.
 */
public abstract class DebouncingOnClickListener implements View.OnClickListener {
  static boolean enabled = true;

  private static final Runnable ENABLE_AGAIN = () -> enabled = true;

  @Override public final void onClick(View v) {
    if (enabled) {
      enabled = false;
      v.post(ENABLE_AGAIN);
      doClick(v);
    }
  }

  public abstract void doClick(View v);
}

This method handles clicks only after previous click has been handled and note that it avoids multiple clicks in a frame.

Piyush Jain
  • 51
  • 1
  • 3
4

Similar Solution using RxJava

import android.view.View;

import java.util.concurrent.TimeUnit;

import rx.android.schedulers.AndroidSchedulers;
import rx.functions.Action1;
import rx.subjects.PublishSubject;

public abstract class SingleClickListener implements View.OnClickListener {
    private static final long THRESHOLD_MILLIS = 600L;
    private final PublishSubject<View> viewPublishSubject = PublishSubject.<View>create();

    public SingleClickListener() {
        viewPublishSubject.throttleFirst(THRESHOLD_MILLIS, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Action1<View>() {
                    @Override
                    public void call(View view) {
                        onClicked(view);
                    }
                });
    }

    @Override
    public void onClick(View v) {
        viewPublishSubject.onNext(v);
    }

    public abstract void onClicked(View v);
}
GaneshP
  • 746
  • 7
  • 25
4

A Handler based throttler from Signal App.

import android.os.Handler;
import android.support.annotation.NonNull;

/**
 * A class that will throttle the number of runnables executed to be at most once every specified
 * interval.
 *
 * Useful for performing actions in response to rapid user input where you want to take action on
 * the initial input but prevent follow-up spam.
 *
 * This is different from a Debouncer in that it will run the first runnable immediately
 * instead of waiting for input to die down.
 *
 * See http://rxmarbles.com/#throttle
 */
public final class Throttler {

    private static final int WHAT = 8675309;

    private final Handler handler;
    private final long    thresholdMs;

    /**
     * @param thresholdMs Only one runnable will be executed via {@link #publish} every
     *                  {@code thresholdMs} milliseconds.
     */
    public Throttler(long thresholdMs) {
        this.handler     = new Handler();
        this.thresholdMs = thresholdMs;
    }

    public void publish(@NonNull Runnable runnable) {
        if (handler.hasMessages(WHAT)) {
            return;
        }

        runnable.run();
        handler.sendMessageDelayed(handler.obtainMessage(WHAT), thresholdMs);
    }

    public void clear() {
        handler.removeCallbacksAndMessages(null);
    }
}

Example usage:

throttler.publish(() -> Log.d("TAG", "Example"));

Example usage in an OnClickListener:

view.setOnClickListener(v -> throttler.publish(() -> Log.d("TAG", "Example")));

Example Kt usage:

view.setOnClickListener {
    throttler.publish {
        Log.d("TAG", "Example")
    }
}

Or with an extension:

fun View.setThrottledOnClickListener(throttler: Throttler, function: () -> Unit) {
    throttler.publish(function)
}

Then example usage:

view.setThrottledOnClickListener(throttler) {
    Log.d("TAG", "Example")
}
weston
  • 54,145
  • 21
  • 145
  • 203
4

You can use Rxbinding3 for that purpose. Just add this dependency in build.gradle

build.gradle

implementation 'com.jakewharton.rxbinding3:rxbinding:3.1.0'

Then in your activity or fragment, use the bellow code

your_button.clicks().throttleFirst(10000, TimeUnit.MILLISECONDS).subscribe {
    // your action
}
Aminul Haque Aome
  • 2,261
  • 21
  • 34
2

I use this class together with databinding. Works great.

/**
 * This class will prevent multiple clicks being dispatched.
 */
class OneClickListener(private val onClickListener: View.OnClickListener) : View.OnClickListener {
    private var lastTime: Long = 0

    override fun onClick(v: View?) {
        val current = System.currentTimeMillis()
        if ((current - lastTime) > 500) {
            onClickListener.onClick(v)
            lastTime = current
        }
    }

    companion object {
        @JvmStatic @BindingAdapter("oneClick")
        fun setOnClickListener(theze: View, f: View.OnClickListener?) {
            when (f) {
                null -> theze.setOnClickListener(null)
                else -> theze.setOnClickListener(OneClickListener(f))
            }
        }
    }
}

And my layout looks like this

<TextView
        app:layout_constraintTop_toBottomOf="@id/bla"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        android:gravity="center"
        android:textSize="18sp"
        app:oneClick="@{viewModel::myHandler}" />
Arneball
  • 359
  • 3
  • 10
1

More significant way to handle this scenario is using Throttling operator (Throttle First) with RxJava2. Steps to achieve this in Kotlin :

1). Dependencies :- Add rxjava2 dependency in build.gradle app level file.

implementation 'io.reactivex.rxjava2:rxandroid:2.1.1'
implementation 'io.reactivex.rxjava2:rxjava:2.2.10'

2). Construct an abstract class that implements View.OnClickListener & contains throttle first operator to handle the view’s OnClick() method. Code snippet is as:

import android.util.Log
import android.view.View
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.subjects.PublishSubject
import java.util.concurrent.TimeUnit

abstract class SingleClickListener : View.OnClickListener {
   private val publishSubject: PublishSubject<View> = PublishSubject.create()
   private val THRESHOLD_MILLIS: Long = 600L

   abstract fun onClicked(v: View)

   override fun onClick(p0: View?) {
       if (p0 != null) {
           Log.d("Tag", "Clicked occurred")
           publishSubject.onNext(p0)
       }
   }

   init {
       publishSubject.throttleFirst(THRESHOLD_MILLIS, TimeUnit.MILLISECONDS)
               .observeOn(AndroidSchedulers.mainThread())
               .subscribe { v -> onClicked(v) }
   }
}

3). Implement this SingleClickListener class on the click of view in activity. This can be achieved as :

override fun onCreate(savedInstanceState: Bundle?)  {
   super.onCreate(savedInstanceState)
   setContentView(R.layout.activity_main)

   val singleClickListener = object : SingleClickListener(){
      override fun onClicked(v: View) {
       // operation on click of xm_view_id
      }
  }
xm_viewl_id.setOnClickListener(singleClickListener)
}

Implementing these above steps into the app can simply avoid the multiple clicks on a view till 600mS. Happy coding!

Abhijeet
  • 501
  • 4
  • 7
1

in Kotlin, we can use this extension

fun View.setOnSingleClickListener(action: (v: View) -> Unit) {
    setOnClickListener { v ->
        isClickable = false
        action(v)
        postDelayed({ isClickable = true }, 700)
    }
}
MarGin
  • 2,078
  • 1
  • 17
  • 28
0

This is solved like this

Observable<Object> tapEventEmitter = _rxBus.toObserverable().share();
Observable<Object> debouncedEventEmitter = tapEventEmitter.debounce(1, TimeUnit.SECONDS);
Observable<List<Object>> debouncedBufferEmitter = tapEventEmitter.buffer(debouncedEventEmitter);

debouncedBufferEmitter.buffer(debouncedEventEmitter)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(new Action1<List<Object>>() {
      @Override
      public void call(List<Object> taps) {
        _showTapCount(taps.size());
      }
    });
shekar
  • 1,251
  • 4
  • 22
  • 31
0

Here is a pretty simple solution, which can be used with lambdas:

view.setOnClickListener(new DebounceClickListener(v -> this::doSomething));

Here is the copy/paste ready snippet:

public class DebounceClickListener implements View.OnClickListener {

    private static final long DEBOUNCE_INTERVAL_DEFAULT = 500;
    private long debounceInterval;
    private long lastClickTime;
    private View.OnClickListener clickListener;

    public DebounceClickListener(final View.OnClickListener clickListener) {
        this(clickListener, DEBOUNCE_INTERVAL_DEFAULT);
    }

    public DebounceClickListener(final View.OnClickListener clickListener, final long debounceInterval) {
        this.clickListener = clickListener;
        this.debounceInterval = debounceInterval;
    }

    @Override
    public void onClick(final View v) {
        if ((SystemClock.elapsedRealtime() - lastClickTime) < debounceInterval) {
            return;
        }
        lastClickTime = SystemClock.elapsedRealtime();
        clickListener.onClick(v);
    }
}

Enjoy!

Leo DroidCoder
  • 14,527
  • 4
  • 62
  • 54
0

Based on @GreyBeardedGeek answer,

  • Create debounceClick_last_Timestamp on ids.xml to tag previous click timestamp.
  • Add This block of code into BaseActivity

    protected void debounceClick(View clickedView, DebouncedClick callback){
        debounceClick(clickedView,1000,callback);
    }
    
    protected void debounceClick(View clickedView,long minimumInterval, DebouncedClick callback){
        Long previousClickTimestamp = (Long) clickedView.getTag(R.id.debounceClick_last_Timestamp);
        long currentTimestamp = SystemClock.uptimeMillis();
        clickedView.setTag(R.id.debounceClick_last_Timestamp, currentTimestamp);
        if(previousClickTimestamp == null 
              || Math.abs(currentTimestamp - previousClickTimestamp) > minimumInterval) {
            callback.onClick(clickedView);
        }
    }
    
    public interface DebouncedClick{
        void onClick(View view);
    }
    
  • Usage:

    view.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            debounceClick(v, 3000, new DebouncedClick() {
                @Override
                public void onClick(View view) {
                    doStuff(view); // Put your's click logic on doStuff function
                }
            });
        }
    });
    
  • Using lambda

    view.setOnClickListener(v -> debounceClick(v, 3000, this::doStuff));
    
Khaled Lela
  • 7,831
  • 6
  • 45
  • 73
0

Put a little example here

view.safeClick { doSomething() }

@SuppressLint("CheckResult")
fun View.safeClick(invoke: () -> Unit) {
    RxView
        .clicks(this)
        .throttleFirst(300, TimeUnit.MILLISECONDS)
        .subscribe { invoke() }
}
Shwarz Andrei
  • 647
  • 14
  • 17
0

My solution, need to call removeall when we exit (destroy) from the fragment and activity:

import android.os.Handler
import android.os.Looper
import java.util.concurrent.TimeUnit

    //single click handler
    object ClickHandler {

        //used to post messages and runnable objects
        private val mHandler = Handler(Looper.getMainLooper())

        //default delay is 250 millis
        @Synchronized
        fun handle(runnable: Runnable, delay: Long = TimeUnit.MILLISECONDS.toMillis(250)) {
            removeAll()//remove all before placing event so that only one event will execute at a time
            mHandler.postDelayed(runnable, delay)
        }

        @Synchronized
        fun removeAll() {
            mHandler.removeCallbacksAndMessages(null)
        }
    }
Cà phê đen
  • 1,883
  • 2
  • 21
  • 20
0

I would say the easiest way is to use a "loading" library like KProgressHUD.

https://github.com/Kaopiz/KProgressHUD

The first thing at the onClick method would be to call the loading animation which instantly blocks all UI until the dev decides to free it.

So you would have this for the onClick action (this uses Butterknife but it obviously works with any kind of approach):

Also, don't forget to disable the button after the click.

@OnClick(R.id.button)
void didClickOnButton() {
    startHUDSpinner();
    button.setEnabled(false);
    doAction();
}

Then:

public void startHUDSpinner() {
    stopHUDSpinner();
    currentHUDSpinner = KProgressHUD.create(this)
            .setStyle(KProgressHUD.Style.SPIN_INDETERMINATE)
            .setLabel(getString(R.string.loading_message_text))
            .setCancellable(false)
            .setAnimationSpeed(3)
            .setDimAmount(0.5f)
            .show();
}

public void stopHUDSpinner() {
    if (currentHUDSpinner != null && currentHUDSpinner.isShowing()) {
        currentHUDSpinner.dismiss();
    }
    currentHUDSpinner = null;
}

And you can use the stopHUDSpinner method in the doAction() method if you so desire:

private void doAction(){ 
   // some action
   stopHUDSpinner()
}

Re-enable the button according to your app logic: button.setEnabled(true);

0

in case anyone is looking for a solution in jetpack compose

class OnThrottledClickHandler(
    private val minClickInterval: Long = DEFAULT_CLICK_INTERVAL,
    private val onClick: () -> Unit,
) {
    private var lastClickTime: Long = 0

    fun processClick() {
        val currentClickTime: Long = System.currentTimeMillis()
        val elapsedTime = currentClickTime - lastClickTime
        lastClickTime = currentClickTime
        if (elapsedTime > minClickInterval) {
            onClick.invoke()
        }
    }

    companion object {
        const val DEFAULT_CLICK_INTERVAL: Long = 600
    }
}

@Composable
fun throttledClickListener(delay: Long = 600L, onClick: () -> Unit): ReadOnlyProperty<Any?, () -> Unit> =
    object : ReadOnlyProperty<Any?, () -> Unit> {
        val onThrottledClickHandler = remember { OnThrottledClickHandler(delay, onClick) }
        override fun getValue(thisRef: Any?, property: KProperty<*>): () -> Unit = onThrottledClickHandler::processClick
    }

how to use:

val onThrottledClick by throttledClickListener(delay = 1200L) {
                onSubmitButtonClicked.invoke()
            }
Button(onClick = onThrottledClick) // pass to your component
Rax
  • 1,347
  • 3
  • 20
  • 27
0
// A modern way to debounce multiple rapid clicks using coroutines

// 1) Create a kotlin util class or file to hold your debounce logic

private var debounceJob: Job? = null

fun debounce(
    scope: CoroutineScope,
    debounceFunction: () -> Unit,
    delayMs: Long = 200L
) {
    
        if (debounceJob == null) {
            debounceJob = scope.launch(Dispatchers.Main) {
                try {
                    debounceFunction()
                    delay(delayMs)
                    debounceJob?.cancel()
                    debounceJob = null
                } catch (e: Exception) {
                    Log.e("YourClass", "Caught $e while trying to debounce")
                }
            }
        }
    }
    

// 2. To use it for your view
findViewById<View>(R.id.yourView)?.setOnClickListener {
            debounce(GlobalScope, { showScoreDetails(scoreInfo) }, BOTTOM_SHEET_DEBOUNCE_MILLIS)
        }
0

You don't have to create a separate Interface or Class for this use case. You can achieve the same by adding this Extension function on the View class.

This function is going to throttle the click event when spammed within 500ms.

fun View.setOnThrottledClickListener(clickListener: View.OnClickListener) {

    var lastClickTime: Long = 0

    setOnClickListener {
        val clickedTime = System.currentTimeMillis()
        if (clickedTime - lastClickTime < 500L) { // 500 ms
            return@setOnClickListener
        }
        clickListener.onClick(it)
        lastClickTime = clickedTime
    }
}

And use it like this

actionButton.setOnThrottledClickListener {
    // Doing some action here
}
OhhhThatVarun
  • 3,981
  • 2
  • 26
  • 49