After doing some research on Coroutines and Flow I came up with solution on creating custom EditText which holds debounce logic inside it and enables me to attach debounce TextWatcher and remove it when I want. Here is the code of my solution
class DebounceEditText @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet? = null,
defStyleAttr: Int = 0
) : AppCompatEditText(context, attributeSet, defStyleAttr) {
private val debouncePeriod = 600L
private var searchJob: Job? = null
@FlowPreview
@ExperimentalCoroutinesApi
fun setOnDebounceTextWatcher(lifecycle: Lifecycle, onDebounceAction: (String) -> Unit) {
searchJob?.cancel()
searchJob = onDebounceTextChanged()
.debounce(debouncePeriod)
.onEach { onDebounceAction(it) }
.launchIn(lifecycle.coroutineScope)
}
fun removeOnDebounceTextWatcher() {
searchJob?.cancel()
}
@ExperimentalCoroutinesApi
private fun onDebounceTextChanged(): Flow<String> = channelFlow {
val textWatcher = object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {}
override fun afterTextChanged(p0: Editable?) {
offer(p0.toString())
}
}
addTextChangedListener(textWatcher)
awaitClose {
removeTextChangedListener(textWatcher)
}
}
}
When I want to activate Debounce TextWatcher, I just call
// lifecycle is passed from Activity/Fragment lifecycle, because we want to relate Coroutine lifecycle with the one DebounceEditText belongs
debounceEditText.setOnDebounceTextWatcher(lifecycle) { input ->
Log.d("DebounceEditText", input)
}
I had problem with focus when implementing DebounceEditText inside xml, so I had to set clickable, selectable and focusableInTouchMode to true.
android:focusable="true"
android:focusableInTouchMode="true"
android:clickable="true"
In case I want to set input in DebounceEditText without triggering, just remove TextWatcher by calling
debounceEditText.removeOnDebounceTextWatcher()