Kotlin Android 去抖动
Posted
技术标签:
【中文标题】Kotlin Android 去抖动【英文标题】:Kotlin Android debounce 【发布时间】:2018-11-24 07:14:56 【问题描述】:有没有什么奇特的方法可以用 Kotlin android 实现 debounce
逻辑?
我没有在项目中使用 Rx。
Java有办法,不过这里对我来说太大了。
【问题讨论】:
您在寻找基于协程的解决方案吗? @AlexeyRomanov 是的,据我所知,它会非常有效。 Throttle onQueryTextChange in SearchView的可能重复 ***.com/a/50007453/2413303 这个人对我标记为重复的问题有一个共同的回答。 @EpicPandaForce 会尝试更新,但问题似乎不同。 【参考方案1】:我创建了一个gist,其中包含受this elegant solution 启发的三个去抖动运算符,来自Patrick,我在其中添加了两个类似的案例: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 中,但 this post 包含兼容的实现。
我这样打电话:protected fun throttleClick(clickAction: (Unit) -> Unit) viewModelScope.launch throttleFirst(scope = this, action = clickAction)
但什么也没发生,它只是返回,返回 fun
的 throttleFirst 不会触发。为什么?
@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 Coroutines 的callbackFlow 和debounce 来实现去抖动。例如,要实现按钮单击事件的去抖动,您可以执行以下操作:
在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我根据堆栈溢出的旧答案创建了一个扩展函数:
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-fd134d54f4e9 和https://***.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)
【讨论】:
以上是关于Kotlin Android 去抖动的主要内容,如果未能解决你的问题,请参考以下文章