Kotlin Android дебаунс

Есть ли какой-нибудь необычный способ реализовать debounce логику с помощью Kotlin Android?

Я не использую Rx в проекте.

Есть способ в Java, но он слишком велик, как для меня здесь.


person Kyryl Zotov    schedule 14.06.2018    source источник
comment
Вы ищете решение на основе сопрограмм?   -  person Alexey Romanov    schedule 14.06.2018
comment
@AlexeyRomanov Да, как я понял, было бы очень эффективно.   -  person Kyryl Zotov    schedule 14.06.2018
comment
Возможный дубликат Дросселирования onQueryTextChange в SearchView   -  person EpicPandaForce    schedule 14.06.2018
comment
stackoverflow.com/a/50007453/2413303 у этого парня был обычный ответ на вопрос, который я пометил как повторяющийся.   -  person EpicPandaForce    schedule 14.06.2018
comment
@EpicPandaForce Попробовал бы и обновил, но вопрос кажется другим.   -  person Kyryl Zotov    schedule 14.06.2018


Ответы (10)


Для этого можно использовать сопрограммы kotlin. Вот пример.

Имейте в виду, что сопрограммы являются экспериментальными на kotlin 1.1+, и он может быть изменен в следующих версиях kotlin.

ОБНОВИТЬ

Начиная с версии Kotlin 1.3, сопрограммы теперь стабильны.

person Diego Malone    schedule 16.06.2018
comment
К сожалению, теперь, когда была выпущена версия 1.3.x, это кажется устаревшим. - person Joshua King; 20.11.2018
comment
@JoshuaKing, да. Возможно, поможет medium.com/@pro100svitlo/. Попробую позже. - person CoolMind; 10.12.2018
comment
Спасибо! Потому что это очень полезно, но мне нужно обновить. Спасибо. - person Joshua King; 10.12.2018
comment
Вы уверены, что каналы считаются стабильными? - person EpicPandaForce; 19.12.2018
comment
Любопытно: почему почти все решения предлагают использование сопрограмм, вынуждая среди остальных добавлять конкретную зависимость (вопрос не говорит о том, что сопрограммы уже используются )? Разве для такой простой операции не добавляются лишние накладные расходы? Разве не лучше использовать System.currentTimeMillis() или подобное? - person Mabsten; 23.05.2021

Я создал суть с тремя операторами противодействия, вдохновленными это элегантное решение от Патрика, где я добавил еще два похожих случаи: throttleFirst и throttleLatest. Оба они очень похожи на свои аналоги RxJava (throttleFirst, throttleLatest).

throttleLatest работает аналогично debounce, но работает с временными интервалами и возвращает последние данные для каждого из них, что позволяет при необходимости получать и обрабатывать промежуточные данные.

fun <T> throttleLatest(
    intervalMs: Long = 300L,
    coroutineScope: CoroutineScope,
    destinationFunction: (T) -> Unit
): (T) -> Unit {
    var throttleJob: Job? = null
    var latestParam: T
    return { param: T ->
        latestParam = param
        if (throttleJob?.isCompleted != false) {
            throttleJob = coroutineScope.launch {
                delay(intervalMs)
                latestParam.let(destinationFunction)
            }
        }
    }
}

throttleFirst полезен, когда вам нужно сразу обработать первый вызов, а затем пропустить последующие вызовы на некоторое время, чтобы избежать нежелательного поведения (например, избегайте запуска двух идентичных действий на Android).

fun <T> throttleFirst(
    skipMs: Long = 300L,
    coroutineScope: CoroutineScope,
    destinationFunction: (T) -> Unit
): (T) -> Unit {
    var throttleJob: Job? = null
    return { param: T ->
        if (throttleJob?.isCompleted != false) {
            throttleJob = coroutineScope.launch {
                destinationFunction(param)
                delay(skipMs)
            }
        }
    }
}

debounce помогает определить состояние, когда новые данные не отправляются в течение некоторого времени, что позволяет эффективно обрабатывать данные после завершения ввода.

fun <T> debounce(
    waitMs: Long = 300L,
    coroutineScope: CoroutineScope,
    destinationFunction: (T) -> Unit
): (T) -> Unit {
    var debounceJob: Job? = null
    return { param: T ->
        debounceJob?.cancel()
        debounceJob = coroutineScope.launch {
            delay(waitMs)
            destinationFunction(param)
        }
    }
}

Все эти операторы можно использовать следующим образом:

val onEmailChange: (String) -> Unit = throttleLatest(
            300L, 
            viewLifecycleOwner.lifecycleScope, 
            viewModel::onEmailChanged
        )
emailView.onTextChanged(onEmailChange)
person Terenfear    schedule 29.07.2019
comment
было бы неплохо с некоторым выходом. - person Kebab Krabby; 29.08.2019
comment
Обратите внимание, что onTextChanged() отсутствует в Android SDK, хотя этот пост содержит совместимая реализация. - person CommonsWare; 29.09.2019
comment
Я звоню так: protected fun throttleClick(clickAction: (Unit) -> Unit) { viewModelScope.launch { throttleFirst(scope = this, action = clickAction) } } но ничего не происходит, он просто возвращается, throttleFirst, возвращающий fun, не срабатывает. Почему? - person GuilhE; 19.12.2019
comment
@GuilhE Эти три функции не приостанавливаются, поэтому вам не нужно вызывать их внутри области сопрограммы. Что касается дросселирования щелчков, я обычно обрабатываю его на стороне фрагмента / активности, что-то вроде val invokeThrottledAction = throttleFirst(lifecycleScope, viewModel::doSomething); button.setOnClickListener { invokeThrottledAction() } В основном вам нужно сначала создать объект функции, а затем вызывать его, когда вам нужно. - person Terenfear; 19.12.2019
comment
Хорошо, @Terenfear, теперь я понял, спасибо за объяснение. Что касается области сопрограммы, поскольку у нас есть задержка, нам нужна область сопрограммы, я просто абстрагировался от вызывающей модели ViewModel (я использую привязку данных), щелчки не создаются в представлении), и этот код находится в BaseViewModel. Или вы говорите, что вместо launch для получения области видимости я мог бы просто вызвать val ViewModel.viewModelScope: CoroutineScope, поскольку я уже внутри ViewModel (если это так, вы правы)? Кстати, действительно полезные функции, спасибо! - person GuilhE; 20.12.2019
comment
Почему при 2-м, 3-м или 4-м вызове и т. Д. Job не считается нулевым? - person I.Step; 16.12.2020
comment
@ I.Step возьмем для примера fun <T> debounce(...), то же самое и с другими функциями. Мы вызываем его один раз, и он создает объект функции типа (T) -> Unit, который чем-то похож на объект анонимного класса Java с одним методом. Ссылка на вакансию содержится внутри этого объекта (вроде как внутри частного поля анонимного класса Java). Каждый раз, когда что-то происходит, мы используем (вызываем) один и тот же функциональный объект. Итак, не имеет значения, сколько раз мы вызываем, это один и тот же объект с той же ссылкой на задание. Возможно, я немного ошибаюсь в терминах, но я так понимаю это. - person Terenfear; 17.12.2020
comment
У меня проблемы с получением viewLifecycleOwner.lifecycleScope внутри адаптера (или даже в моем фрагменте!). Откуда это взялось? Я смог найти viewLifecycleOwner.lifecycle во фрагменте, но LifecycleScope нет. - person Alan Nelson; 11.02.2021
comment
@AlanNelson viewLifecyleOwner.lifecycleScope происходит из androidx.lifecycle:lifecycle-runtime-ktx:2.2.0. Дополнительная информация: developer.android.com/topic/libraries/architecture/ - person Terenfear; 12.02.2021

Я использую callbackFlow и отменить от Kotlin Coroutines для устранения неполадок. Например, чтобы добиться устранения ошибки при нажатии кнопки, вы делаете следующее:

Создайте метод расширения на Button для создания callbackFlow:

fun Button.onClicked() = callbackFlow<Unit> {
    setOnClickListener { offer(Unit) }
    awaitClose { setOnClickListener(null) }
}

Подпишитесь на события в рамках вашей активности или фрагмента, относящегося к жизненному циклу. Следующий фрагмент кода обрабатывает события кликов каждые 250 мс:

buttonFoo
    .onClicked()
    .debounce(250)
    .onEach { doSomethingRadical() }
    .launchIn(lifecycleScope)
person masterwok    schedule 25.02.2020

Для простого подхода из ViewModel вы можете просто запустить задание в viewModelScope, отслеживать задание и отменить его, если новое значение появляется до завершения задания:

private var searchJob: Job? = null

fun searchDebounced(searchText: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(500)
        search(searchText)
    }
}
person Chris Chute    schedule 24.10.2020

Более простое и универсальное решение - использовать функцию, которая возвращает функцию, которая выполняет логику противодействия, и сохраняет ее в val.

fun <T> debounce(delayMs: Long = 500L,
                   coroutineContext: CoroutineContext,
                   f: (T) -> Unit): (T) -> Unit {
    var debounceJob: Job? = null
    return { param: T ->
        if (debounceJob?.isCompleted != false) {
            debounceJob = CoroutineScope(coroutineContext).launch {
                delay(delayMs)
                f(param)
            }
        }
    }
}

Теперь его можно использовать с:

val handleClickEventsDebounced = debounce<Unit>(500, coroutineContext) {
    doStuff()
}

fun initViews() {
   myButton.setOnClickListener { handleClickEventsDebounced(Unit) }
}
person Patrick    schedule 23.04.2019
comment
Я следую вашей логике при написании кода отказа. Однако в моем случае doStuff () принимает параметры. Есть ли способ передать параметры при вызове handleClickEventsDebounced, который затем передается в doStuff ()? - person tech_human; 16.11.2019
comment
Конечно, этот фрагмент поддерживает это. В приведенном выше примере handleClickEventsDebounced(Unit) Unit - это параметр. Это может быть любой тип, который вам нужен, поскольку мы используем дженерики. Для строки, например, сделайте следующее: val handleClickEventsDebounced = debounce ‹String› = debounce ‹String› (500, coroutineContext) {doStuff (it)} Где 'it' - переданная строка. Или назовите его {myString - ›doStuff (myString)} - person Patrick; 20.11.2019
comment
Привет, @Patrick, можешь мне помочь с этим: stackoverflow.com/q/59413001/1423773? Бьюсь об заклад, в нем отсутствует какая-то мелочь, но я не могу ее найти. - person GuilhE; 19.12.2019
comment
Это напомнило мне о реализации Underscore в JavaScript, которая мне очень нравится за простоту. Поздравления и спасибо! - person Machado; 19.04.2020
comment
Эта реализация не устраняет дребезг, а регулирует. - person Simon; 09.03.2021

Я создал одну функцию расширения из старых ответов о переполнении стека:

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()
        }
    })
}

Просмотрите onClick, используя приведенный ниже код:

buttonShare.clickWithDebounce { 
   // Do anything you want
}
person SANAT    schedule 05.06.2019
comment
Да! Я не знаю, почему так много ответов усложняют это с помощью сопрограмм. - person Tenfour04; 05.03.2021

Спасибо https://medium.com/@pro100svitlo/edittext-debounce-with-kotlin-coroutines-fd134d54f4e9 и https://stackoverflow.com/a/50007453/2914140 Я написал такой код:

private var textChangedJob: Job? = null
private lateinit var textListener: TextWatcher

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                          savedInstanceState: Bundle?): View? {

    textListener = object : TextWatcher {
        private var searchFor = "" // Or view.editText.text.toString()

        override fun afterTextChanged(s: Editable?) {}

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            val searchText = s.toString().trim()
            if (searchText != searchFor) {
                searchFor = searchText

                textChangedJob?.cancel()
                textChangedJob = launch(Dispatchers.Main) {
                    delay(500L)
                    if (searchText == searchFor) {
                        loadList(searchText)
                    }
                }
            }
        }
    }
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)

    editText.setText("")
    loadList("")
}


override fun onResume() {
    super.onResume()
    editText.addTextChangedListener(textListener)
}

override fun onPause() {
    editText.removeTextChangedListener(textListener)
    super.onPause()
}


override fun onDestroy() {
    textChangedJob?.cancel()
    super.onDestroy()
}

Я не включил здесь coroutineContext, поэтому, вероятно, он не будет работать, если не установлен. Для получения информации см. Переход на сопрограммы Kotlin в Android с Kotlin 1.3.

person CoolMind    schedule 10.12.2018
comment
Извините, я понял, что если мы запустим несколько запросов, они вернутся асинхронно. Таким образом, не гарантируется, что последний запрос вернет последние данные, и вы обновите представление с правильными данными. - person CoolMind; 27.03.2019
comment
Также, как я понял, textChangedJob?.cancel() не отменяет запрос в Retrofit. Итак, будьте готовы к тому, что вы получите все ответы на все запросы в случайной последовательности. - person CoolMind; 06.06.2019
comment
Возможно, поможет новая версия Retrofit (2.6.0). - person CoolMind; 06.06.2019

Использование тегов кажется более надежным способом, особенно при работе с RecyclerView.ViewHolder представлениями.

e.g.

fun View.debounceClick(debounceTime: Long = 1000L, action: () -> Unit) {
    setOnClickListener {
        when {
            tag != null && (tag as Long) > System.currentTimeMillis() -> return@setOnClickListener
            else -> {
                tag = System.currentTimeMillis() + debounceTime
                action()
            }
        }
    }
}

Использование:

debounceClick {
    // code block...
}
person Rodrigo Queiroz    schedule 03.02.2020

Ответ @ masterwork работал отлично. Вот он для ImageButton с удаленными предупреждениями компилятора:

@ExperimentalCoroutinesApi // This is still experimental API
fun ImageButton.onClicked() = callbackFlow<Unit> {
    setOnClickListener { offer(Unit) }
    awaitClose { setOnClickListener(null) }
}

// Listener for button
val someButton = someView.findViewById<ImageButton>(R.id.some_button)
someButton
    .onClicked()
    .debounce(500) // 500ms debounce time
    .onEach {
        clickAction()
    }
    .launchIn(lifecycleScope)
person Knight Forked    schedule 18.04.2020

@masterwork,

Отличный ответ. Это моя реализация для динамической панели поиска с EditText. Это обеспечивает значительное повышение производительности, поэтому поисковый запрос не выполняется сразу при вводе текста пользователем.

    fun AppCompatEditText.textInputAsFlow() = callbackFlow {
        val watcher: TextWatcher = doOnTextChanged { textInput: CharSequence?, _, _, _ ->
            offer(textInput)
        }
        awaitClose { [email protected](watcher) }
    }
        searchEditText
                .textInputAsFlow()
                .map {
                    val searchBarIsEmpty: Boolean = it.isNullOrBlank()
                    searchIcon.isVisible = searchBarIsEmpty
                    clearTextIcon.isVisible = !searchBarIsEmpty
                    viewModel.isLoading.value = true
                    return@map it
                }
                .debounce(750) // delay to prevent searching immediately on every character input
                .onEach {
                    viewModel.filterPodcastsAndEpisodes(it.toString())
                    viewModel.latestSearch.value = it.toString()
                    viewModel.activeSearch.value = !it.isNullOrBlank()
                    viewModel.isLoading.value = false
                }
                .launchIn(lifecycleScope)
    }
person Rvb84    schedule 19.05.2020