Kotlin Android 去抖动

Kotlin Android debounce

有什么奇特的方法可以用 Kotlin Android 实现 debounce 逻辑吗?

我没有在项目中使用 Rx。

Java有方法,但对我来说太大了

您可以使用 kotlin 协程 来实现。 Here is an example.

请注意,协程experimental at kotlin 1.1+,它可能会在即将推出的 kotlin 版本中更改。

更新

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

感谢 https://medium.com/@pro100svitlo/edittext-debounce-with-kotlin-coroutines-fd134d54f4e9 and 我写了这段代码:

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,因此如果未设置,它可能无法正常工作。有关信息,请参阅 Migrate to Kotlin coroutines in Android with Kotlin 1.3

一个更简单和通用的解决方案是使用一个函数 returns 一个执行去抖动逻辑的函数,并将其存储在一个 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) }
}

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

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
}

我创建了一个 gist with three debounce operators inspired by from Patrick where I added two more similar cases: throttleFirst and throttleLatest. Both of these are very similar to their RxJava analogues (throttleFirst, throttleLatest)。

throttleLatest 的工作方式与 debounce 类似,但它按时间间隔运行,并且 returns 每个数据的最新数据,这允许您在需要时获取和处理中间数据.

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)

使用标签似乎是一种更可靠的方式,尤其是在使用 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...
}

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

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

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

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

buttonFoo
    .onClicked()
    .debounce(250)
    .onEach { doSomethingRadical() }
    .launchIn(lifecycleScope)

@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 { 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)
    }

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

private var searchJob: Job? = null

fun searchDebounced(searchText: String) {
    searchJob?.cancel()
    searchJob = viewModelScope.launch {
        delay(500)
        search(searchText)
    }
}