You added one small attribute to an EditText. The app compiled fine. You ran it, typed a few characters, and — crash. No warning, no obvious reason, just a stacktrace pointing somewhere deep in the Android framework.
EditText is one of the most-used components in Android and also one of the most quietly dangerous. It inherits from TextView but doesn't always behave like one. It has edge cases around input types, text watchers, and focus handling that cause crashes in ways that feel completely unexpected the first time you hit them.
This post covers the most common EditText crashes — including the classic textAllCaps trap — with the exact fix for each one.
Crash 1 — IndexOutOfBoundsException with textAllCaps
This is the most common one. You have a form where all input should be uppercase — a vehicle number field, an ID input, a promo code box. You apply textAllCaps because it works perfectly on TextViews. The app crashes the moment the user types.
The crash
java.lang.IndexOutOfBoundsException: setSpan (X ... Y) ends beyond length Z
at android.text.SpannableStringBuilder.checkRange(SpannableStringBuilder.java)
Why it happens
textAllCaps is a TextView-only property. It works by replacing the text with an AllCapsTransformationMethod that transforms displayed text to uppercase. But EditText manages its own cursor and span positions internally — when the transformation shifts character positions, the cursor index goes out of bounds. EditText inherits textAllCaps from TextView but was never designed to work with it.
❌ Wrong
<!-- This WILL crash when the user types -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAllCaps="true" />
✅ Fix — use inputType instead
<!-- Forces uppercase keyboard input — no crash -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textCapCharacters" />
Or in Kotlin if you need to set it programmatically:
// Programmatic equivalent of textCapCharacters
binding.etInput.inputType = InputType.TYPE_CLASS_TEXT or
InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS
textCapCharacters— ALL CAPStextCapWords— First Letter Of Each WordtextCapSentences— First letter of each sentence
Crash 2 — StackOverflowError in TextWatcher
You add a TextWatcher to format input as the user types — a phone number, a credit card, a currency field. You call setText() inside afterTextChanged(). The app freezes then crashes with a StackOverflowError.
Why it happens
Calling setText() inside afterTextChanged() triggers afterTextChanged() again. Which calls setText() again. Which triggers afterTextChanged() again. Infinite loop. Stack overflow.
❌ Wrong
binding.etPhone.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
// Calling setText here re-triggers afterTextChanged — infinite loop!
binding.etPhone.setText(formatPhoneNumber(s.toString()))
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
✅ Fix - use a guard flag
var isFormatting = false
binding.etPhone.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
if (isFormatting) return // Guard — prevents re-entry
isFormatting = true
val formatted = formatPhoneNumber(s.toString())
binding.etPhone.setText(formatted)
// Move cursor to end after formatting
binding.etPhone.setSelection(formatted.length)
isFormatting = false
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
Crash 3 — Memory Leak from TextWatcher in Fragment
This one doesn't crash immediately — it leaks memory silently. Eventually your app slows down, consumes excess RAM, and may crash from an OutOfMemoryError after extended use. The culprit is a TextWatcher that holds a reference to the Fragment after the Fragment's view is destroyed.
Why it happens
TextWatchers are registered on the EditText view. If you add a TextWatcher in onViewCreated() but never remove it, the TextWatcher keeps a reference to your Fragment (through this or lambda capture). When the Fragment navigates away, the view is destroyed but the TextWatcher — and therefore the Fragment — can't be garbage collected.
❌ Wrong
class SearchFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Added but never removed — leaks Fragment reference
binding.etSearch.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
performSearch(s.toString()) // captures Fragment context
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
})
}
}
✅ Fix — remove in onDestroyView()
class SearchFragment : Fragment() {
private val searchWatcher = object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
performSearch(s.toString())
}
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.etSearch.addTextChangedListener(searchWatcher)
}
override fun onDestroyView() {
super.onDestroyView()
// Always remove TextWatcher when view is destroyed
binding.etSearch.removeTextChangedListener(searchWatcher)
}
}
Crash 4 — NullPointerException on getText()
You call editText.text.toString() and get a NullPointerException. Seems impossible — text should always return something. But there's one case where it returns null: when the EditText hasn't been fully initialised yet, or when you're accessing it before the layout is inflated.
✅ Fix — use safe call or orEmpty()
// Safe — never throws NPE val input = binding.etInput.text?.toString().orEmpty() // Or with a trim for form validation val input = binding.etInput.text?.toString()?.trim() ?: "" // Kotlin extension to make it cleaner everywhere fun EditText.value(): String = text?.toString()?.trim() ?: "" // Usage val email = binding.etEmail.value() val password = binding.etPassword.value()
Crash 5 — Cursor Crash with Mixed inputType Flags
You set inputType to a combination of flags that conflict with each other. The most common example: combining textPassword with textMultiLine. The keyboard behaves strangely, the cursor jumps, and on some devices it crashes.
Why it happens
textPassword hides characters and disables multi-line input at the OS level. Combining it with textMultiLine creates a contradictory state that different OEM keyboards handle differently — some crash, some just misbehave.
❌ Wrong
<!-- textPassword and textMultiLine are contradictory -->
<EditText
android:inputType="textPassword|textMultiLine" />
✅ Fix — pick one or use numberPassword for PINs
<!-- Password field — single line only -->
<EditText
android:inputType="textPassword" />
<!-- Numeric PIN -->
<EditText
android:inputType="numberPassword" />
<!-- Multi-line notes field -->
<EditText
android:inputType="textMultiLine"
android:minLines="3"
android:maxLines="6"
android:gravity="top|start" />
Quick Reference — Common inputType Values
| Use Case | inputType |
|---|---|
| Email address | textEmailAddress |
| Password | textPassword |
| Numeric PIN | numberPassword |
| Phone number | phone |
| Decimal number | numberDecimal |
| ALL CAPS text | textCapCharacters |
| Multi-line text | textMultiLine |
| URL | textUri |
Best Practices
- Never use textAllCaps on EditText — use
inputType="textCapCharacters"instead. This is the single most common EditText crash and it's 100% avoidable. - Always guard TextWatcher with a flag — any time you call
setText()inside a TextWatcher, wrap the logic in anisFormattingguard to prevent infinite recursion. - Always remove TextWatcher in onDestroyView() — store your TextWatcher as a property and remove it when the Fragment's view is destroyed to prevent memory leaks.
- Use safe calls on getText() —
editText.text?.toString().orEmpty()is safer thaneditText.text.toString(). Create an extension functionEditText.value()and use it everywhere. - Don't mix contradictory inputType flags —
textPasswordandtextMultiLinedon't mix. Read the Android docs for which flags are compatible before combining them.
Frequently Asked Questions
Why does textAllCaps crash EditText?
textAllCaps uses a TransformationMethod designed for TextView that shifts character positions. EditText manages cursor spans internally — when those positions go out of sync, it throws IndexOutOfBoundsException. Use inputType="textCapCharacters" instead.
How do I make EditText input uppercase?
Use android:inputType="textCapCharacters" in XML, or InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS in Kotlin. This tells the keyboard to show uppercase without modifying spans internally.
How do I fix StackOverflowError in TextWatcher?
It happens when you call setText() inside afterTextChanged(), triggering an infinite loop. Add a boolean isFormatting guard — return early if it's true, set it before calling setText(), and reset it after.
How do I prevent memory leaks with TextWatcher in Fragment?
Store the TextWatcher as a Fragment property and call removeTextChangedListener() in onDestroyView(). This releases the reference to the Fragment so it can be garbage collected when the view is destroyed.
- textAllCaps on EditText → use
inputType="textCapCharacters"instead - StackOverflowError in TextWatcher → add
isFormattingguard before callingsetText() - TextWatcher memory leak in Fragment → store as property, remove in
onDestroyView() - NullPointerException on getText() → use
text?.toString().orEmpty()or an extension function - Conflicting inputType flags → never combine
textPasswordwithtextMultiLine