gist,其灵感来自于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)
}
}
}
}
当您需要立即处理第一个调用,然后跳过后续调用一段时间以避免出现不良行为(例如,避免在 Android 上启动两个相同的活动)时,
throttleFirst
非常有用。
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)
ViewModel
内部进行的简单方法,您可以在
viewModelScope
内启动一项作业,跟踪该作业,如果在作业完成之前出现新值则取消它:
private var searchJob: Job? = null
fun searchDebounced(searchText: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(500)
search(searchText)
}
}
Kotlin Coroutines 中的 callbackFlow 和 debounce 来实现去抖动。例如,要实现按钮单击事件的去抖动,请执行以下操作:
在Button
上创建扩展方法以生成
callbackFlow
:
fun Button.onClicked() = callbackFlow<Unit> {
setOnClickListener { offer(Unit) }
awaitClose { setOnClickListener(null) }
}
订阅生命周期感知活动或片段中的事件。以下代码片段每 250 毫秒消除一次点击事件:
buttonFoo
.onClicked()
.debounce(250)
.onEach { doSomethingRadical() }
.launchIn(lifecycleScope)
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 协程。
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...
}
很好的答案。这是我使用 EditText 实现的动态搜索栏。这提供了巨大的性能改进,因此搜索查询不会立即在用户文本输入上执行。
fun AppCompatEditText.textInputAsFlow() = callbackFlow {
val watcher: TextWatcher = doOnTextChanged { textInput: CharSequence?, _, _, _ ->
offer(textInput)
}
awaitClose { [email protected](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)
}
@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)
用途:
ActionDelayer.delay { println("重复...") }
定义单例对象
object DebounceHelper {
private var job: Job? = null
fun <T> debounce(
delayMs: Long = 500L,
scope: CoroutineScope,
func: (T) -> Unit
): (T) -> Unit {
job?.cancel()
return { param: T ->
job = scope.launch {
delay(delayMs)
func(param)
}
}
}
}
如何使用
DebounceHelper.debounce<Unit>(2000, scope) {
anotherFunc()
}.invoke(Unit)
fun EditText.debounce(delay: Long, action: (CharSequence?) -> Unit) {
doAfterTextChanged { text ->
val counter = getTag(id) as? Int ?: 0
handler.removeCallbacksAndMessages(counter)
handler.postDelayed(delay, ++counter) { action(text) }
setTag(id, counter)
}
}
用途
val editText = findViewById<EditText>(R.id.editText)
editText.debounce(500) {
if (it.isNotEmpty()) {
// Submit the form
}
}