Search code examples
androidandroid-edittexttextwatcherandroid-textwatcher

Avoid EditText TextWatcher firing events on every character change


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.


Solution

  • 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);
                }
            }
        });
    }