Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Android EditText Coroutine debounce operator like RxJava

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.

like image 388
Ban Markovic Avatar asked Aug 15 '20 13:08

Ban Markovic


Video Answer


2 Answers

Turn text change event to a 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) }
}

Use:

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

like image 104
Petrus Nguyễn Thái Học Avatar answered Oct 21 '22 22:10

Petrus Nguyễn Thái Học


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()
like image 4
Ban Markovic Avatar answered Oct 21 '22 22:10

Ban Markovic