【问题标题】:Kotlin Android debounceKotlin Android 去抖动
【发布时间】:2018-11-24 07:14:56
【问题描述】:

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

我没有在项目中使用 Rx。

Java有办法,不过这里对我来说太大了。

【问题讨论】:

标签: android kotlin kotlin-android-extensions kotlin-extension kotlinx.coroutines


【解决方案1】:

我创建了一个gist,其中包含受this elegant solution 启发的三个去抖动运算符,来自Patrick,我在其中添加了两个类似的案例:throttleFirstthrottleLatest。这两者都与它们的 RxJava 类似物(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)

【讨论】:

  • 有一些输出就好了。
  • 请注意,onTextChanged() 不在 Android SDK 中,但 this post 包含兼容的实现。
  • 我这样打电话:protected fun throttleClick(clickAction: (Unit) -&gt; Unit) { viewModelScope.launch { throttleFirst(scope = this, action = clickAction) } } 但什么也没发生,它只是返回,返回 funthrottleFirst 不会触发。为什么?
  • @GuilhE 这三个函数没有暂停,因此您不必在协程范围内调用它们。至于节流点击,我通常在片段/活动端处理,比如val invokeThrottledAction = throttleFirst(lifecycleScope, viewModel::doSomething); button.setOnClickListener { invokeThrottledAction() } 基本上你必须先创建一个函数对象,然后在需要时调用它。
  • 好的@Terenfear 我现在明白了,谢谢你的解释。关于协程范围,由于我们有一个延迟,我们需要一个协程范围,我刚刚从 ViewModel 调用者(我正在使用数据绑定)中抽象出点击不是在视图中创建的)并且代码在“基础视图模型”。或者你是说我可以调用val ViewModel.viewModelScope: CoroutineScope 而不是launch 来获得一个范围,因为我已经在一个ViewModel 中(如果是这样,你是对的)?顺便说一句,非常有用的功能,谢谢!
【解决方案2】:

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

private var searchJob: Job? = null

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

【讨论】:

    【解决方案3】:

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

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

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

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

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

    【讨论】:

      【解决方案4】:

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

      【讨论】:

      • 我正在按照你的逻辑来编写我的去抖动代码。但是在我的情况下, doStuff() 接受参数。有没有一种方法可以在调用“handleClickEventsDebounced”时传递参数,然后传递给 doStuff()?
      • 当然,这个 sn-p 支持它。在上面的示例中,handleClickEventsDebounced(Unit) Unit 是参数。它可以是您想要的任何类型,因为我们使用泛型。例如,对于字符串,请执行以下操作: val handleClickEventsDebounced = debounce = debounce(500, coroutineContext) { doStuff(it) } 其中 'it' 是传递的字符串。或者用 { myString -> doStuff(myString) } 命名
      • 你好@Patrick,你能帮我解决这个问题吗:stackoverflow.com/q/59413001/1423773?我敢打赌它缺少一个小细节,但我找不到它。
      • 这让我想起了 JavaScript 的 Underscore 实现,我非常喜欢它的简单性。恭喜,谢谢!
      • 此实现不会反跳,但会节流。
      【解决方案5】:

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

      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
      }
      

      【讨论】:

      • 是的!我不知道为什么这里有这么多答案让协程复杂化。
      【解决方案6】:

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

      【讨论】:

      • 对不起,我明白了,如果我们运行多个查询,它们会异步返回。因此,不能保证最后一个请求将返回最后一个数据,并且您将使用正确的数据更新视图。
      • 另外,据我了解,textChangedJob?.cancel() 不会取消 Retrofit 中的请求。因此,请准备好以随机顺序从所有请求中获得所有响应。
      • 可能新版本的 Retrofit (2.6.0) 会有所帮助。
      【解决方案7】:

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

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

      更新

      自从Kotlin 1.3 发布以来,协程现在已经稳定了。

      【讨论】:

      • 不幸的是,现在 1.3.x 已经发布,这似乎已经过时了。
      • @JoshuaKing,是的。也许medium.com/@pro100svitlo/… 会有所帮助。我稍后再试。
      • 谢谢!因为这非常有用,但我需要更新。谢谢。
      • 您确定频道被认为是稳定的吗?
      • 一个好奇:为什么几乎所有的解决方案都建议使用 coroutines,迫使其他解决方案添加特定的依赖项(问题不说协程已经在使用)?对于这样一个简单的操作,他们不是增加了不必要的开销吗?使用 System.currentTimeMillis() 或类似的不是更好吗?
      【解决方案8】:

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

      【讨论】:

        【解决方案9】:

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

        【讨论】:

          【解决方案10】:

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

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-05-28
            • 1970-01-01
            • 1970-01-01
            • 2015-06-28
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多