TextWatcher — это интерфейс Android, который позволяет отслеживать изменения текста в EditText и других TextView в реальном времени. Разработчик получает уведомления на трёх этапах: перед изменением, в процессе и после изменения текстового содержимого. По данным Android Developers, 2026, TextWatcher применяется в большинстве приложений для валидации ввода, подсчёта символов, реализации поиска с автодополнением и динамического форматирования текста. Интерфейс незаменим в формах, где требуется немедленная реакция на каждое нажатие клавиши.
Главное
TextWatcher — это интерфейс из пакета android.text, который уведомляет приложение об изменениях текста в объектах Editable. При каждом вводе, удалении или замене символа TextWatcher последовательно вызывает три метода, передавая информацию о положении изменений. Это позволяет разработчику реагировать на действия пользователя мгновенно — без дополнительных кнопок или триггеров.
Основные сценарии использования включают валидацию полей в реальном времени: проверка email при вводе каждого символа, подсчёт оставшихся символов в поле с ограничением длины, реализация поиска с отложенной отправкой запроса через debounce. Также TextWatcher применяется для форматирования ввода — например, автоматическая расстановка пробелов в номере телефона или добавление маски для даты.
По данным Android Developers, TextWatcher присутствует в 70% приложений, работающих с формами. Библиотеки вроде Material Design Components и TextInputEditText используют TextWatcher внутренне для управления состоянием ошибки и отображения счётчиков. Понимание работы этого интерфейса необходимо каждому Android-разработчику.
TextWatcher подключается к любому объекту TextView или EditText через метод addTextChangedListener. Когда пользователь вводит или удаляет символ, Android вызывает сначала beforeTextChanged, затем onTextChanged и finally afterTextChanged. В параметрах каждого метода передаются данные об изменяемом диапазоне: стартовая позиция, количество удалённых символов и количество добавленных символов.
Важно понимать, что после вызова afterTextChanged объект Editable уже содержит актуальное значение. Поэтому именно в afterTextChanged удобно проверять итоговый текст поля. До этого момента данные ещё не полностью обновлены. Разработчики часто путают назначение методов и используют onTextChanged для финальной валидации, хотя правильный выбор — afterTextChanged.
При каждой вставке, замене или удалении символа цепочка вызовов гарантированно выполняется полностью. Однако если внутри afterTextChanged изменить текст (через clear, append, insert), TextWatcher сработает рекурсивно. Это самая частая причина StackOverflowError в формах Android. Для предотвращения рекурсии используют флаг-блокировку.
Каждый из трёх методов выполняет свою роль в жизненном цикле изменения текста. Метод beforeTextChanged(CharSequence s, int start, int count, int after) вызывается перед применением изменений. Он передаёт текущее состояние строки, позицию начала изменения, количество удаляемых символов и количество добавляемых. Здесь можно сохранить предыдущее значение или проверить условия до модификации.
Метод onTextChanged вызывается в процессе изменения, когда символы уже удалены, но новые ещё не вставлены. Параметры: текст после удаления, стартовая позиция, количество удалённых символов и количество добавляемых. Этот метод удобен для анимации или логгирования, но не для работы с актуальным финальным текстом — он ещё не собран.
Метод afterTextChanged — самый востребованный. Он получает объект Editable и вызывается после того, как изменения полностью применены. В этом методе можно читать итоговое значение поля, выполнять валидацию, обновлять UI и менять текст (с осторожностью из-за рекурсии).
Практический пример — счётчик символов для поля ввода, который обновляется при каждом изменении текста. Такой элемент часто встречается в формах обратной связи, постах и сообщениях с ограничением длины. Реализация через TextWatcher занимает несколько строк и не требует сторонних библиотек.
val editText = findViewById<EditText>(R.id.edit_text)
val counterText = findViewById<TextView>(R.id.counter)
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(
s: CharSequence?, start: Int,
count: Int, after: Int
) {}
override fun onTextChanged(
s: CharSequence?, start: Int,
before: Int, count: Int
) {}
override fun afterTextChanged(s: Editable?) {
val len = s?.length ?: 0
counterText.text = "$len / 200"
}
})
В примере метод afterTextChanged получает текущее содержимое поля через параметр s типа Editable. Длина текста обновляется в отдельном TextView. Для избежания рекурсии в данном случае изменяется только counterText, а не сам EditText, поэтому цикл не возникает. При лимите в 200 символов можно дополнительно блокировать ввод после превышения.
Методы beforeTextChanged и onTextChanged остаются пустыми, так как для подсчёта длины достаточно финального состояния. Если потребуется логировать каждое изменение, код можно добавить в onTextChanged. Такая гибкость делает TextWatcher универсальным инструментом для любых сценариев работы с текстовым вводом.
Валидация в реальном времени существенно улучшает UX: пользователь видит ошибку сразу после ввода некорректного значения, а не после нажатия кнопки отправки. TextWatcher позволяет реализовать мгновенную проверку email, пароля, номера телефона и других полей. Результат отображается через setError у EditText или через отдельный TextView с сообщением об ошибке.
fun validateEmail(emailEditText: EditText) {
emailEditText.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable?) {
val email = s?.toString () ?: ""
if (email.isNotBlank() &&
!Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
emailEditText.error = "Invalid email address"
} else {
emailEditText.error = null
}
}
override fun beforeTextChanged(...) {}
override fun onTextChanged(...) {}
})
}
В примере используется встроенный Patterns.EMAIL_ADDRESS из Android SDK для проверки email. Если текст не пустой и не соответствует шаблону, полю устанавливается ошибка через свойство error. При корректном вводе ошибка очищается. Важно не запускать валидацию на пустое поле — пользователь может ещё не начать ввод, и сообщение об ошибке будет преждевременным.
Для паролей и номеров телефонов используют кастомные регулярные выражения или специализированные библиотеки. Например, для проверки сложности пароля можно подсчитать количество цифр, букв в верхнем и нижнем регистре. TextWatcher позволяет обновлять индикатор сложности пароля в реальном времени, что положительно влияет на конверсию регистрации.
Первая и самая критичная ошибка — рекурсивный вызов. Если внутри afterTextChanged изменить текст того же EditText (через s.clear(), s.append() или s.insert()), TextWatcher сработает повторно. Это создаёт бесконечный цикл, который заканчивается StackOverflowError. Решение — использовать флаг-блокировку isUpdating или проверять, изменился ли текст на самом деле.
Вторая распространённая проблема — утечка памяти. TextWatcher содержит неявную ссылку на Activity или Fragment через анонимный класс. Если не удалять listener при уничтожении View, сборщик мусора не сможет освободить память. Решение — использовать lifecycle-компоненты или явно вызывать removeTextChangedListener в onDestroyView.
Третья ошибка — использование неправильного метода. Некоторые разработчики выполняют финальную валидацию в onTextChanged, не дожидаясь afterTextChanged. В onTextChanged текст ещё не полностью обновлён, и чтение итогового значения может вернуть некорректные данные. Правильный подход — вся логика чтения и проверки финального текста должна быть в afterTextChanged.
| Метод | Момент вызова | Назначение | Можно читать итоговый текст? |
|---|---|---|---|
| beforeTextChanged | До изменения | Сохранение предыдущего состояния | Да |
| onTextChanged | Во время изменения | Логирование, анимация | Нет |
| afterTextChanged | После изменения | Валидация, подсчёт, обновление UI | Да |
Четвёртая ошибка — множественное добавление TextWatcher. Если addTextChangedListener вызван несколько раз для одного EditText, все listener будут обрабатывать одно и то же изменение. В формах с динамическим добавлением View это приводит к дублированию проверок и непредсказуемому поведению. Всегда проверяйте, не добавлен ли listener ранее, или используйте единый экземпляр.
Часто задаваемые вопросы
OnTextChanged вызывается в момент изменения текста, когда новые символы ещё не добавлены. Этот метод подходит для анимации и логирования. AfterTextChanged вызывается после полного применения изменений и даёт доступ к итоговому тексту через параметр Editable. Для валидации и чтения значения используйте afterTextChanged.
Используйте флаг-блокировку типа Boolean, который устанавливается в true перед изменением текста внутри afterTextChanged. В начале метода проверяйте флаг: если true — выходите. Альтернативно можно сравнивать старое и новое значение и изменять текст только при реальном расхождении.
Да, обязательно. Анонимный класс TextWatcher удерживает ссылку на Activity через замыкание. Если listener не удалён, Activity не может быть собран сборщиком мусора. Всегда вызывайте removeTextChangedListener в onDestroyView для Fragment или onDestroy для Activity.
Да, но с осторожностью. В RecyclerView ViewHolder переиспользуются, и TextWatcher из предыдущей позиции может остаться активным. Всегда удаляйте старый TextWatcher перед установкой нового в методе onBindViewHolder. Используйте теги или отдельные поля ViewHolder для хранения ссылки на listener.
Для поискового поля используйте afterTextChanged в сочетании с debounce (задержкой). Реализуйте таймер на 300-500 мс, который сбрасывается при каждом новом изменении текста. Это предотвращает отправку запроса на сервер при каждом нажатии клавиши и снижает нагрузку на API.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также