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

Posted

技术标签:

【中文标题】防止快速单击按钮并使用 rxjava 发出请求【英文标题】:Preventing rapid clicks on a button and making a request using rxjava 【发布时间】:2020-10-30 00:33:21 【问题描述】:

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

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

基本上,如果用户在 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)
    

【问题讨论】:

您提出一个问题,阅读 2 周的答案,然后发布自己的一半答案并接受?我想从技术上讲,您所做的事情没有任何问题,但是,您是否知道在您自己的回答中,如果稍后提供该响应,您可能会得到一个过时的口袋妖怪版本?至少 2 个答案告诉您如何正确完成这项工作。请至少在您的答案中添加一个 switchMap。 感谢您提及 RxBinding。 【参考方案1】:

基本上,如果用户快速点击 按钮在 300 毫秒内它应该接受最后一个 点击该持续时间

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

Debounce — 仅在特定的情况下从 Observable 发射一个项目 时间跨度已经过去,它没有发出另一个项目

可以看弹珠图here

【讨论】:

我确实尝试过 debouce 运算符。但是,我认为可以将持续时间设置为特定时间。但我认为你对这个案子可能是对的。【参考方案2】:
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

【讨论】:

感谢您的回答。我已将我的答案发布为您的稍作修改的版本。这按预期工作。我还发布了我认为最适合我使用 RxBinding 库的解决方案的最终答案。【参考方案3】:

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 调用处于活动状态。

【讨论】:

【参考方案4】:

还有另一种有趣的方法可以实现这一点 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 调用,从而优化资源消耗。

【讨论】:

【参考方案5】:

我认为您应该将点击事件作为一个可观察的事件来处理,这样您就可以在点击本身上调用 debounce。实现您所寻找的一种方法是让一个类从被点击的视图中创建一个 observable:

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;
    


在活动/片段中:

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

【讨论】:

【参考方案6】:

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

import  fromEvent  from 'rxjs';

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

因此,使用包含油门的订阅链,但从 fromEvent 方法开始。它把一个事件变成了一个可观察的对象。 (我不确定你会在哪里创建它,但在 C# 中,我们都在类构造函数中完成。)

【讨论】:

【参考方案7】:

感谢大家的回答。

但是,我找到了一个使用 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

【讨论】:

【参考方案8】:

这是基于@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)

【讨论】:

以上是关于防止快速单击按钮并使用 rxjava 发出请求的主要内容,如果未能解决你的问题,请参考以下文章

快速连续发出多个请求时 RestKit 崩溃

使用 axios 向服务器发出请求会冻结 React Native 应用程序

Retrofit + RxJava 中的链接请求

Python - 使用 Python 3 urllib 发出 POST 请求

Python - 使用Python 3 urllib发出POST请求

RxJava 手动发出项目