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.
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.
我创建了一个包含三个 debounce 运算符的要点,灵感来自Patrick的这个优雅的解决方案,我在其中添加了两个类似的案例:和. 这两者都与它们的 RxJava 类似物(throttleFirst、throttleLatest)非常相似。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)
对于 a 内部的简单方法ViewModel
,您可以在 中启动一个作业viewModelScope
,跟踪该作业,如果在作业完成之前出现新值,则取消它:
private var searchJob: Job? = null
fun searchDebounced(searchText: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(500)
search(searchText)
}
}
我使用Kotlin Coroutines中的callbackFlow和debounce来实现 debounce。例如,要实现按钮单击事件的去抖动,您可以执行以下操作:
在上创建扩展方法Button
以生成callbackFlow
:
fun Button.onClicked() = callbackFlow<Unit> {
setOnClickListener { offer(Unit) }
awaitClose { setOnClickListener(null) }
}
订阅您的生命周期感知活动或片段中的事件。以下代码段每 250 毫秒对点击事件进行一次去抖动:
buttonFoo
.onClicked()
.debounce(250)
.onEach { doSomethingRadical() }
.launchIn(lifecycleScope)
一个更简单和通用的解决方案是使用一个函数,该函数返回一个执行去抖动逻辑的函数,并将其存储在一个 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
}
感谢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 1.3 在 Android 中迁移到 Kotlin 协程。
您可以使用kotlin 协程来实现这一点。 这是一个例子。
请注意,协程在 kotlin 1.1+中是实验性的,并且可能会在即将发布的 kotlin 版本中进行更改。
自Kotlin 1.3发布以来,协程现在很稳定。
使用标签似乎是一种更可靠的方式,尤其是在使用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...
}
@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)
@大师作品,
很好的答案。这是我对带有 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)
}