42

Is there any fancy way to implement debounce logic with Kotlin Android?

I'm not using Rx in project.

There is a way in Java, but it is too big as for me here.

4

10 回答 10

43

我创建了一个包含三个 debounce 运算符的要点,灵感来自Patrick的这个优雅的解决方案,我在其中添加了两个类似的案例:和. 这两者都与它们的 RxJava 类似物(throttleFirstthrottleLatest)非常相似。throttleFirstthrottleLatest

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)
于 2019-07-29T11:22:39.013 回答
29

对于 a 内部的简单方法ViewModel,您可以在 中启动一个作业viewModelScope,跟踪该作业,如果在作业完成之前出现新值,则取消它:

private var searchJob: Job? = null

fun searchDebounced(searchText: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(500)
        search(searchText)
    }
}
于 2020-10-24T23:37:10.490 回答
20

我使用Kotlin Coroutines中的callbackFlowdebounce来实现 debounce。例如,要实现按钮单击事件的去抖动,您可以执行以下操作:

在上创建扩展方法Button以生成callbackFlow

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

订阅您的生命周期感知活动或片段中的事件。以下代码段每 250 毫秒对点击事件进行一次去抖动:

buttonFoo
    .onClicked()
    .debounce(250)
    .onEach { doSomethingRadical() }
    .launchIn(lifecycleScope)
于 2020-02-25T03:05:32.900 回答
18

一个更简单和通用的解决方案是使用一个函数,该函数返回一个执行去抖动逻辑的函数,并将其存储在一个 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) }
}
于 2019-04-23T00:57:16.993 回答
12

我从堆栈溢出的旧答案创建了一个扩展函数:

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
}
于 2019-06-05T14:28:45.830 回答
9

感谢https://medium.com/@pro100svitlo/edittext-debounce-with-kotlin-coroutines-fd134d54f4e9https://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 1.3 在 Android 中迁移到 Kotlin 协程

于 2018-12-10T10:37:31.873 回答
8

您可以使用kotlin 协程来实现这一点。 这是一个例子

请注意,协程在 kotlin 1.1+中是实验性的,并且可能会在即将发布的 kotlin 版本中进行更改。

更新

Kotlin 1.3发布以来,协程现在很稳定。

于 2018-06-16T16:28:30.987 回答
5

使用标签似乎是一种更可靠的方式,尤其是在使用RecyclerView.ViewHolder视图时。

例如

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...
}
于 2020-02-03T01:52:03.380 回答
3

@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)
于 2020-04-18T08:14:56.047 回答
3

@大师作品,

很好的答案。这是我对带有 EditText 的动态搜索栏的实现。这提供了极大的性能改进,因此搜索查询不会立即在用户文本输入时执行。

    fun AppCompatEditText.textInputAsFlow() = callbackFlow {
        val watcher: TextWatcher = doOnTextChanged { textInput: CharSequence?, _, _, _ ->
            offer(textInput)
        }
        awaitClose { this@textInputAsFlow.removeTextChangedListener(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)
    }
于 2020-05-19T08:19:45.810 回答