I need to make EditText with autosuggest functionality and I need to listen to its input. I also need to ignore EditText change when it is set programmatically.
Wondering if there is solution to make debounce EditText with Coroutines without using delay in this situation.
Flow
Use kotlinx.coroutines 1.5.0
@ExperimentalCoroutinesApi
@CheckResult
fun EditText.textChanges(): Flow<CharSequence?> {
return callbackFlow<CharSequence?> {
val listener = object : TextWatcher {
override fun afterTextChanged(s: Editable?) = Unit
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
trySend(s)
}
}
addTextChangedListener(listener)
awaitClose { removeTextChangedListener(listener) }
}.onStart { emit(text) }
}
If using androidx.core.widget.doOnTextChanged
:
@ExperimentalCoroutinesApi
@CheckResult
fun EditText.textChanges(): Flow<CharSequence?> {
return callbackFlow {
checkMainThread()
val listener = doOnTextChanged { text, _, _, _ -> trySend(text) }
awaitClose { removeTextChangedListener(listener) }
}.onStart { emit(text) }
}
editText.textChanges().debounce(300)
.onEach { ... }
.launchIn(lifecycleScope)
And something like this:
fun executeSearch(term: String): Flow<SearchResult> { ... }
editText.textChanges()
.filterNot { it.isNullOrBlank() }
.debounce(300)
.distinctUntilChanged()
.flatMapLatest { executeSearch(it) }
.onEach { updateUI(it) }
.launchIn(lifecycleScope)
Source code: https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/blob/master/core-ui/src/main/java/com/hoc/flowmvi/core_ui/FlowBinding.kt#L116
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()
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With