Search code examples
androidkotlinlistener

Android Kotlin editText Listener Question


I'm trying to program something where a user inputs a number into an editText field and it automatically has decimal places appended. I'm using editText.setText inside the listener, but find that the code crashes every time. Upon logging the value that I'm trying to edit, I found that the moment I change the editText field, editText.setText runs non-stop, which causes the program to crash. Below is my code:

val etTesting = findViewById<EditText>(R.id.etTesting)
        etTesting.addTextChangedListener(object : TextWatcher {
            override fun afterTextChanged(s: Editable?) {}

            override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}

            override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
                if (!s.isNullOrEmpty() && s.toString() != ".") {
                    val value = s.toString().toDouble()
                    val formatted = String.format("%.2f", value)
                    Log.i(TAG, "onTextChanged $formatted")
                    etTesting.setText(formatted)
                    etTesting.setSelection(formatted.length)
                }
            }
        })

I tried to use setText outside of the listener and it doesn't trigger a crash. It's only inside the listener. I'm guessing it has to do with the listener writing an updated value, seeing that it's updated again, and then goes through this sequence infinitely?

How is one supposed to use the listener to do what I'm trying to do?

Thanks for the help.


Solution

  • As a more general approach (instead of relying on filters, which aren't always enough for what you want to do) you could create some kind of updating flag in your TextWatcher:

    object : TextWatcher {
        var updating = false
    
        override fun afterTextChanged(s: Editable?) {}
        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
    
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
            // skip reacting to the text change if it's caused by an internal update
            if (!updating) {
                updating = true
                // make changes to the text contents
                updating = false
            }
        }
    }
    

    Basically when onTextChanged fires, it checks to see if there's an internal update happening, and only proceeds if there isn't. First it sets that updating flag, and then updates the text, which immediately triggers the TextWatcher callbacks again.

    Those calls are recursive, so your initial onTextChanged function won't complete until the triggered onTextChanged returns. And if that fires another onTextChanged, you end up with infinite nested function calls until you get a (heyo) stack overflow.

    By using this flag, you short-circuit that behaviour. Because the first call sets that updating flag, the text change will trigger a second call, which sees that updating is true, and returns without making any changes. Then your first onTextChanged call will complete. And the last thing it does is set updating to false since the update process has finished, so it's ready to handle the next change.