Предотвращение быстрых нажатий на кнопку и отправка запроса с помощью rxjava - PullRequest
6 голосов
/ 09 июля 2020

У меня есть следующий метод, который делает запрос на получение покемона из конечной точки.

Я хотел бы запретить пользователю делать быстрые запросы, быстро нажимая кнопку, которая будет вызывать этот метод много раз . Я использовал методы 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)
    }

Ответы [ 8 ]

7 голосов
/ 09 июля 2020

По сути, то, что я ищу, если пользователь быстро нажимает кнопку в течение 300 миллисекунд, он должен принять последний щелчок в этой продолжительности

для меня больше похоже на этот дребезг поведение оператора. Из документации

Debounce - испускать элемент из Observable только в том случае, если определенный промежуток времени прошел, но он не испустил другого элемента

вы можете увидеть мраморную диаграмму здесь

4 голосов
/ 20 июля 2020
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.

3 голосов
/ 14 июля 2020

getPokemonDetailByName() подписывается на новый поток каждый раз, когда он вызывается.

Вместо того, чтобы каждый раз подписываться на новый поток, просто укажите объект для отправки данных и сопоставьте его напрямую с LiveData с LiveDataReactiveStreams.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.

3 голосов
/ 14 июля 2020

Есть еще один интересный подход для достижения этой цели т.е. 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)
            }
        }
    }
}

Таким образом, если пользователь нажимает кнопку безостановочно с интервалом менее 300 мс, getPokemonDetailByName(name) не звонит. Таким образом, только последний щелчок вызывает функцию.

Это очень полезно также там, где у нас есть окно поиска, которое запускает запрос на основе текста, введенного пользователем. ( ig добавление TextWatcher на EditText) Это приводит к меньшему количеству вызовов api, когда пользователь набирает текст, что оптимизирует потребление ресурсов.

3 голосов
/ 14 июля 2020

Я думаю, вы должны обрабатывать события кликов как наблюдаемые, чтобы вы могли вызывать debounce для самих кликов. Один из способов добиться того, что вы ищете, - создать класс, создающий наблюдаемый объект из выбранного представления:

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(... );

Обновить : Спасибо Амиту Шекхару за эту статью: Реализовать поиск с использованием Rx Java Операторы

2 голосов
/ 20 июля 2020

У всех есть сложные способы создать наблюдаемое из нажатия кнопки. Rx js, похоже, имеет встроенный способ сделать это с самой первой страницы :

import { fromEvent } from 'rxjs';

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

Итак, возьмите свою цепочку подписок, которая включает дроссель, но начните с fromEvent метод. Он превращает событие в наблюдаемое для вас. (Я не уверен, где вы бы это создали, но в C# мы делаем все это в конструкторе класса.)

0 голосов
/ 21 июля 2020

Это основано на ответе @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)
}
0 голосов
/ 21 июля 2020

Спасибо за все ответы.

Однако я нашел решение, которое работает с использованием 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
}
...