Есть ли какой-нибудь необычный способ реализовать debounce
логику с помощью Kotlin Android?
Я не использую Rx в проекте.
Есть способ в Java, но он слишком велик, как для меня здесь.
Есть ли какой-нибудь необычный способ реализовать debounce
логику с помощью Kotlin Android?
Я не использую Rx в проекте.
Есть способ в Java, но он слишком велик, как для меня здесь.
Для этого можно использовать сопрограммы kotlin. Вот пример.
Имейте в виду, что сопрограммы являются экспериментальными на kotlin 1.1+, и он может быть изменен в следующих версиях kotlin.
Начиная с версии Kotlin 1.3, сопрограммы теперь стабильны.
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)
onTextChanged()
отсутствует в Android SDK, хотя этот пост содержит совместимая реализация.
- person CommonsWare; 29.09.2019
protected fun throttleClick(clickAction: (Unit) -> Unit) { viewModelScope.launch { throttleFirst(scope = this, action = clickAction) } }
но ничего не происходит, он просто возвращается, throttleFirst, возвращающий fun
, не срабатывает. Почему?
- person GuilhE; 19.12.2019
val invokeThrottledAction = throttleFirst(lifecycleScope, viewModel::doSomething); button.setOnClickListener { invokeThrottledAction() }
В основном вам нужно сначала создать объект функции, а затем вызывать его, когда вам нужно.
- person Terenfear; 19.12.2019
launch
для получения области видимости я мог бы просто вызвать val ViewModel.viewModelScope: CoroutineScope
, поскольку я уже внутри ViewModel (если это так, вы правы)? Кстати, действительно полезные функции, спасибо!
- person GuilhE; 20.12.2019
fun <T> debounce(...)
, то же самое и с другими функциями. Мы вызываем его один раз, и он создает объект функции типа (T) -> Unit
, который чем-то похож на объект анонимного класса Java с одним методом. Ссылка на вакансию содержится внутри этого объекта (вроде как внутри частного поля анонимного класса Java). Каждый раз, когда что-то происходит, мы используем (вызываем) один и тот же функциональный объект. Итак, не имеет значения, сколько раз мы вызываем, это один и тот же объект с той же ссылкой на задание. Возможно, я немного ошибаюсь в терминах, но я так понимаю это.
- person Terenfear; 17.12.2020
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)
Для простого подхода из ViewModel
вы можете просто запустить задание в viewModelScope
, отслеживать задание и отменить его, если новое значение появляется до завершения задания:
private var searchJob: Job? = null
fun searchDebounced(searchText: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(500)
search(searchText)
}
}
Более простое и универсальное решение - использовать функцию, которая возвращает функцию, которая выполняет логику противодействия, и сохраняет ее в 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) }
}
handleClickEventsDebounced(Unit)
Unit
- это параметр. Это может быть любой тип, который вам нужен, поскольку мы используем дженерики. Для строки, например, сделайте следующее: val handleClickEventsDebounced = debounce ‹String› = debounce ‹String› (500, coroutineContext) {doStuff (it)} Где 'it' - переданная строка. Или назовите его {myString - ›doStuff (myString)}
- person Patrick; 20.11.2019
Я создал одну функцию расширения из старых ответов о переполнении стека:
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
}
Спасибо 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.
textChangedJob?.cancel()
не отменяет запрос в Retrofit. Итак, будьте готовы к тому, что вы получите все ответы на все запросы в случайной последовательности.
- 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...
}
Ответ @ 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)
@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)
}