防止快速点击按钮并使用 rxjava 发出请求

Preventing rapid clicks on a button and making a request using rxjava

我有以下方法可以发出从端点获取口袋妖怪的请求。

我想阻止用户通过快速单击将多次调用此方法的按钮来发出快速请求。我已经使用了 throttle* 方法和 debounce。

基本上,如果用户在 300 毫秒的持续时间内快速点击按钮,我正在寻找的是它应该接受该持续时间内的最后一次点击。但是,我遇到的是所有请求都在提出。也就是说,如果用户在这段时间内快速点击 3 次,我仍然会收到 3 个请求。

   fun getPokemonDetailByName(name: String) {
        pokemonDetailInteractor.getPokemonDetailByName(name)
            .subscribeOn(pokemonSchedulers.background())
            .observeOn(pokemonSchedulers.ui())
            .toObservable()
            .throttleFirst(300, TimeUnit.MILLISECONDS)
            .singleOrError()
            .subscribeBy(
                onSuccess = { pokemon ->
                    pokemonDetailLiveData.value = pokemon
                },
                onError = {
                    Timber.e(TAG, it.localizedMessage)
                }
            ).addTo(compositeDisposable)
    }

Bascially, what I am looking for if the user rapidly clicks on the button within 300 milliseconds duration it should accept the last click in that duration

对我来说听起来更像是去抖操作员的行为。来自文档

Debounce — only emit an item from an Observable if a particular timespan has passed without it emitting another item

可以看到弹珠图here

我认为您应该将点击事件作为可观察事件来处理,这样您就可以对点击本身调用去抖动。实现您所寻找的目标的一种方法是让 class 从被单击的视图中创建一个可观察对象:

public class RxClickObservable {

    public static Observable<String> fromView(View view, String pokemonName) {

        final PublishSubject<String> subject = PublishSubject.create();

        view.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                subject.onNext(pokemonName);
            }
        });

        return subject;
    }

}

并在 activity/fragment 中:

RxClickObservable.fromView(binding.button, pokemonName)
        .subscribeOn(pokemonSchedulers.background())
        .observeOn(pokemonSchedulers.ui())
        .debounce(300, TimeUnit.MILLISECONDS)
        .switchMap(pokemonName ->  pokemonDetailInteractor.getPokemonDetailByName(pokemonName))
        .subscribe(... );

更新: 感谢 Amit Shekhar 撰写本文:Implement Search Using RxJava Operators

还有另一种有趣的方法可以实现这一点 WatchDog。这个概念来自电子和硬件设计。 (要查看有关它的更多信息,请参阅 wikipedia

WatchDog 的要点是,如果 WatchDog 直到适当的时间才重置,委托的工作就会完成。

但是,我们可以将这个概念实现如下:


TimerWatchDog.kt

import java.util.*

/**
 * @author aminography
 */
class TimerWatchDog(private val timeout: Long) {

    private var timer: Timer? = null

    fun refresh(job: () -> Unit) {
        timer?.cancel()
        timer = Timer().also {
            it.schedule(object : TimerTask() {
                override fun run() = job.invoke()
            }, timeout)
        }
    }

    fun cancel() = timer?.cancel()

}

用法:

class MyFragment : Fragment {

    private val watchDog = TimerWatchDog(300)

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        button.setOnClickListener {
            watchDog.refresh {
                getPokemonDetailByName(name)
            }
        }
    }
}

这样,如果用户持续点击按钮,间隔小于300ms,getPokemonDetailByName(name)就不会调用。因此,只有最后一次点击才会调用该函数。

如果我们有一个基于用户输入文本运行查询的搜索框,这也非常有用。 (i.g.EditText 上添加 TextWatcher)它会减少用户输入时的 api 调用,从而优化资源消耗。

getPokemonDetailByName()每次调用都会订阅一个新的流。

不必每次都订阅一个新流,只需提供一个主题以将数据发送到并将其直接映射到 LiveDataLiveDataReactiveStreams.fromPublisher()

private val nameSubject = PublishSubject.create<String>()

val pokemonDetailLiveData = nameSubject.distinctUntilChanged()
                .observeOn(pokemonSchedulers.background())
                .switchMap(pokemonDetailInteractor::getPokemonDetailByName)
                .doOnError { Timber.e(TAG, it.localizedMessage) }
                .onErrorResumeNext(Observable.empty())
                .toFlowable(BackpressureStrategy.LATEST)
                .to(LiveDataReactiveStreams::fromPublisher)

fun getPokemonDetailByName(name: String) {
    nameSubject.onNext(name)
}

需要 observeOn(pokemonSchedulers.background()) 运算符,因为主题处理订阅的方式不同。 onErrorResumeNext(Observable.empty()) 确保只有有效对象最终出现在 LiveData.

像这样,一旦 pokemonDetailLiveData 被观察到,就只订阅一个流。 PublishSubject 确保只有用户点击才会触发来自 API 的更新,并且只有一个 API 调用同时处于活动状态。

private val subject = PublishSubject.create<String>()

init {
    processClick()
}

fun onClick(name: String) {
    subject.onNext(name)
}

private fun processClick() {
    subject
        .debounce(300, TimeUnit.MILLISECONDS)
        .switchMap { getPokemonDetailByName(it) }
        .subscribe(
            { pokemonDetailLiveData.value = it },
            { Timber.e(TAG, it.localizedMessage) }
        )
}

private fun getPokemonDetailByName(name: String): Observable<Pokemon> =   
     pokemonDetailInteractor
        .getPokemonDetailByName(name)
        .subscribeOn(pokemonSchedulers.background())
        .observeOn(pokemonSchedulers.ui())
        .toObservable()

在您的例子中,getPokemonDetailByName 每次都会创建一个新订阅。相反,将点击事件发送到 Subject,创建对该流的单个订阅并应用 debounce.

每个人都有复杂的方法来通过单击按钮创建可观察对象。 Rxjs 似乎从一开始就有内置的方法来做到这一点 page:

import { fromEvent } from 'rxjs';

fromEvent(document, 'click').subscribe(() => console.log('Clicked!'));

因此,采用包含限制的订阅链,但从 fromEvent 方法开始。它把一个事件变成你可以观察到的。 (我不确定你在哪里创建它,但在 C# 中我们在 class 构造函数中完成所有操作。)

感谢大家的回答。

但是,我找到了一个使用 RxBinding 和 debounce 运算符的解决方案。我在这里发帖是因为它可能对其他人有用。

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PokemonViewHolder {
    binding = PokemonListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
    val pokemonViewHolder = PokemonViewHolder(binding.root)

    pokemonViewHolder.itemView.clicks()
        .debounce(300, TimeUnit.MILLISECONDS)
        .subscribeBy(
            onNext = {
                val name = pokemonList[pokemonViewHolder.adapterPosition].name

                if(::pokemonTapped.isInitialized) {
                    pokemonTapped(name)
                }
            },
            onError = { Timber.e(it, "Failed to send pokemon request %s", it.localizedMessage) }
        ).addTo(compositeDisposable)

    return pokemonViewHolder
}

这是基于@ckunder 的回答,稍作修改后按预期工作。

    // Start listening for item clicks when viewmodel created
    init {
        observeOnItemClicks()
    }

    // remove clicks that are emitted during the 200ms duration. In the onNext make the actual request  
    private fun observeOnItemClicks() {
        subject
            .debounce(300, TimeUnit.MILLISECONDS)
            .subscribeBy(
                onNext = { pokemonName ->
                    getPokemonDetailByName(pokemonName)
                },
                onError = { Timber.e(it, "Pokemon click event failed ${it.localizedMessage}")}
            )
            .addTo(compositeDisposable)
    }

// No need to change this as this will be called in the onNext of the subject's subscribeBy
fun getPokemonDetailByName(name: String) {
    shouldShowLoading.postValue(true)

    pokemonDetailInteractor.getPokemonDetailByName(name)
        .subscribeOn(pokemonSchedulers.background())
        .observeOn(pokemonSchedulers.ui())
        .subscribeBy(
            onSuccess = { pokemon ->
                shouldShowLoading.postValue(false)
                pokemonDetailLiveData.postValue(pokemon)
            },
            onError = {
                shouldShowLoading.value = false
                Timber.e(TAG, it.localizedMessage)
            }
        ).addTo(compositeDisposable)
}

// emit the name when the user clicks on an pokemon item in the list
fun onPokemonItemClicked(name: String) {
    subject.onNext(name)
}