Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Debounce input from an EditText and relay to a TextView with a timeout.
package me.vishna.kdebounce
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.EditText
import android.widget.TextView
import kotlinx.coroutines.experimental.*
import kotlinx.coroutines.experimental.android.UI
import kotlinx.coroutines.experimental.channels.*
import java.util.concurrent.TimeUnit
class MainActivity : AppCompatActivity() {
lateinit var inputText: EditText
lateinit var outputText: TextView
var broadcastChannel: BroadcastChannel<String>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
inputText = findViewById(R.id.input_text)
outputText = findViewById(R.id.output_text)
launch(CommonPool) {
broadcastChannel = inputText.textChanged.consumeDebounced(800, TimeUnit.MILLISECONDS) {
launch(UI) { outputText.text = it }
}
}
}
override fun onDestroy() {
broadcastChannel?.close()
super.onDestroy()
}
}
public val EditText.textChanged: BroadcastChannel<String>
get() = EditTextBroadcastChannel(this)
private class EditTextBroadcastChannel(
val editText: EditText,
val broadcastChannel: BroadcastChannel<String> = ConflatedBroadcastChannel())
: BroadcastChannel<String> by broadcastChannel {
init {
launch(UI) {
editText.addTextChangedListener(textWatcher)
}
}
val textWatcher = object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
if (!isClosedForSend) {
offer(editable.toString())
}
}
override fun beforeTextChanged(sequence: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(sequence: CharSequence?, start: Int, before: Int, count: Int) {}
}
override fun close(cause: Throwable?): Boolean {
editText.removeTextChangedListener(textWatcher)
return broadcastChannel.close(cause)
}
}
/**
* Subscribes to this [BroadcastChannel] and performs the specified action for received elements omitting those
* elements that are published too quickly in succession. This is done by dropping elements which are
* followed up by other elements before a specified timer has expired. If the timer expires and no follow up element was received (yet)
* the last received element is passed to the specified action.
*/
public suspend fun <E> BroadcastChannel<E>.consumeDebounced(timeout: Long, unit: TimeUnit = TimeUnit.MILLISECONDS, action: suspend (E) -> Unit): BroadcastChannel<E> {
openSubscription().use { channel ->
var job: Job? = null
var last = System.currentTimeMillis()
for (x in channel) {
val now = System.currentTimeMillis()
val diff = now - last
job?.cancel()
if (!channel.isClosedForReceive) {
job = launch {
if (diff < timeout) {
delay(Math.max(diff, 0), unit)
}
action(x)
last = now
}
}
}
}
return this
}
@mochadwi

This comment has been minimized.

Copy link

commented Jun 3, 2019

Awesome!!! thank you very much @vishna

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.