Debouncing button clicks using Rx

12,991

Note the following in the documentation on the debounce operator:

This variant operates by default on the computation Scheduler (...)

Or, code-wise, this currently happens:

public final Observable<T> debounce(long timeout, TimeUnit unit) {
    return debounce(timeout, unit, Schedulers.computation());
}

As a result, the subscriber's callbacks are invoked on that same computation scheduler, since nothing is explicitly instructing otherwise.

Now, attempting to update a view (that's what's happening in onNext()) from any other thread than the main/ui thread, is a mistake and it will lead to undetermined results.

Fortunately, the remainder of the quote above provides the solution too:

(...) but you can optionally pass in a Scheduler of your choosing as a third parameter.

This would lead to:

RxView.clicks(mButton)
    .debounce(300, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
    .subscribe(...);

Alternatively, you can still let the debounce happen on the computation scheduler, but receive the notifications on the main/ui thread:

RxView.clicks(mButton)
    .debounce(300, TimeUnit.MILLISECONDS)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(...);

Either way will ensure that the notifications are received on the main/ui thread and thus that the view is updated from the correct thread.

Share:
12,991
Hadi Satrio
Author by

Hadi Satrio

"Programmer-designer hybrid with a passion in OOP and mobile application development." Likes: Coffee, Clean Code, Android, Android Studio, ThinkPads, RxJava, MVP, Nexus, kitties. Dislikes: Cold coffee, unformatted codes.. oh and those butt-itch you got from sitting too long.

Updated on July 22, 2022

Comments

  • Hadi Satrio
    Hadi Satrio almost 2 years

    I'm trying to make a simple "button debouncer" which will count filtered clicks and display it thru a TextView. I want to filter rapid/spam clicks in a way that clicks with less than 300ms time-gap in-between are ignored.

    I did my research and stumbled upon Rx's awesome debounce() which in theory should do the exact thing I wanted..

    ..or so I thought. As the app seemed to only register the first click; the counter won't increment no matter how long I tried to wait.

    Here's a piece of my code:

        ...
    
        RxView.clicks(mButton)
            .debounce(300, TimeUnit.MILLISECONDS)
            .subscribe(new Subscriber<Object>() {
                public int mCount;
    
                @Override
                public void onCompleted() {
                }
    
                @Override
                public void onError(Throwable e) {
                }
    
                @Override
                public void onNext(Object o) {
                    mText.setText(String.valueOf(++mCount));
                }
            });
    
        ...
    

    What am I doing wrong? I've tried to run the thing without debounce() and it worked flawlessly (the counter will increment everytime the button got clicked).

    Thanks in advance!

  • wilddev
    wilddev about 7 years
    It's better to use throttleFirst instead of debounce
  • MH.
    MH. about 7 years
    @wilddev: whether it is "better" or not depends on the desired behaviour, as throttleFirst and debounce do not yield the same result. I originally interpreted the question to require debounce, but reading it again now, I agree that throttleFirst could also make sense. I guess that makes both good candidates. Regardless, both operate on the Computation scheduler, so most of above applies either way. :)
  • wilddev
    wilddev about 7 years
    For "better" I mean that in case of throttleFirst you've got click reaction immediately, but with debounce there is a delay. Just to inform others who will find this question later :)
  • MH.
    MH. about 7 years
    True, but since throttleFirst is basically a specific flavour of sample, it will pass through every first item emitted within a certain periodic time interval. debounce has different behaviour, because it only emits an item iff a particular timespan has passed without another item having been emitted. Subtle difference: timespan vs interval. This may or may not be relevant to a particular use case. Just be aware of this.
  • superuser
    superuser about 7 years
    @MH. In RxJava 1.x throttleFirst only uses computation scheduler's time for calculation, but result delivery remains the same as what immediate scheduler does, not checked Rx 2.x source.