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

@mochadwi mochadwi commented Jun 3, 2019

Awesome!!! thank you very much @vishna

@irfanirawansukirman

This comment has been minimized.

Copy link

@irfanirawansukirman irfanirawansukirman commented Nov 27, 2019

Ternyata mengalami hal serupa haha

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.