Avoid EditText TextWatcher firing events on every character change
Asked Answered
J

1

3

I use an EditText textField with TextWatcher attached. All of the callback methods fire events on every character I enter. In my case I call an api on text changed, to put the response into a label on the same fragment. I can not check the EditText for a certain format as it will be freetext.

Example: Whats your name?

E -> api call 
R -> api call
I _> api call
K -> api call

How to avoid that? Can I use a timer (eg 2 seconds no text changed -> api call)? Maybe there is an elegant way of doing that.

Jari answered 8/1, 2022 at 12:19 Comment(2)
What are you using Java or kotlin?Aquilar
You will want to look at using "debounce" techniques.Aramenta
S
5

If you are using Kotlin, you can do this with a coroutine. For example, if you want to run the API call 2 seconds after the text has stopped changing it would look like this:

In the Activity or Fragment with a ViewModel

val model: MainViewModel by viewModels()

binding.textInput.doAfterTextChanged { e ->
    e?.let { model.registerChange( it.toString() ) }
}

In the ViewModel

private var job: Job? = null

fun registerChange(s: String) {
    job?.cancel() // cancel prior job on a new change in text
    job = viewModelScope.launch {
        delay(2_000)
        println("Do API call with $s")
    }
}

Or without using a ViewModel

private var job: Job? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    val binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)

    binding.textInput.doAfterTextChanged { e ->
        e?.let { 
            job?.cancel()
            job = lifecycleScope.launch {
                delay(1_000)
                println("Do API call with ${it}")
            }
        }
    }
}

Using Jetpack Compose

The solution using Jetpack Compose is the same, just added to the onValueChange lambda instead of doAfterTextChanged (either in place, or the same ViewModel call as before).

private var job: Job? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContent {
        val textState = remember { mutableStateOf("") }
        TextField(
            value = textState.value,
            onValueChange = {
                textState.value = it
                job?.cancel()
                job = lifecycleScope.launch {
                    delay(2_000)
                    println("TEST: Do API call with $it")
                }
            }
        )
    }
}

Java Solution

If you are using Java, you could use a handler and a postDelayed runnable instead but the flow would be the same (on a text change, cancel the prior task and start a new one with a delay).

private final Handler handler = new Handler();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    EditText textInput = findViewById(R.id.text_input);
    textInput.addTextChangedListener(new TextWatcher() {
        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {}

        @Override
        public void afterTextChanged(Editable s) {
            if( s != null ) {
                handler.removeCallbacksAndMessages(null);
                handler.postDelayed(() -> {
                    System.out.println("Call API with " + s.toString());
                }, 2000);
            }
        }
    });
}
Shela answered 8/1, 2022 at 15:14 Comment(4)
Thanks Tyler! The solution is technically what I was searching for and the delay in potential error messages is appealing.Jari
This works perfectly for a debounce, thank you!Sling
If we are already cancelling previous job, then why 2 second delay is needed?O
The delay prevents it from calling the API too many times (e.g. after every single text change). With this, it waits to call the API until after 2 seconds of not typing. Every time it the text changes, it cancels the old job and starts a new job to wait 2 seconds, then call the API. If the user types more in that time frame, the currently waiting job is canceled and the API is not called from that job.Shela

© 2022 - 2024 — McMap. All rights reserved.