EditText
Character Count
TextWatcher
Android Development
Input Listener

Counting Chars in EditText Changed Listener

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions

Introduction

Android's TextWatcher interface provides three callback methods that fire as text changes in an EditText. To count characters, use the afterTextChanged callback — it fires after the modification is complete, giving you the final text to measure. Pair it with a TextView to show a live character count, and optionally enforce a maximum length by trimming excess input inside the callback.

TextWatcher Basics

java
1editText.addTextChangedListener(new TextWatcher() {
2    @Override
3    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
4        // Called BEFORE the text changes
5        // s = current text, count = chars about to be replaced, after = replacement length
6    }
7
8    @Override
9    public void onTextChanged(CharSequence s, int start, int before, int count) {
10        // Called DURING the change
11        // s = text with the change applied, count = number of new chars
12    }
13
14    @Override
15    public void afterTextChanged(Editable s) {
16        // Called AFTER the change is complete
17        // s = final text — best place to count characters
18    }
19});

Use afterTextChanged for character counting because the Editable reflects the final state of the text.

Basic Character Counter (Java)

java
1final int MAX_CHARS = 280;
2final TextView charCount = findViewById(R.id.charCount);
3final EditText editText = findViewById(R.id.editText);
4
5editText.addTextChangedListener(new TextWatcher() {
6    @Override
7    public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
8
9    @Override
10    public void onTextChanged(CharSequence s, int start, int before, int count) {}
11
12    @Override
13    public void afterTextChanged(Editable s) {
14        int remaining = MAX_CHARS - s.length();
15        charCount.setText(remaining + " characters remaining");
16
17        if (remaining < 0) {
18            charCount.setTextColor(Color.RED);
19        } else {
20            charCount.setTextColor(Color.GRAY);
21        }
22    }
23});

Kotlin Version

kotlin
1val maxChars = 280
2val charCount: TextView = findViewById(R.id.charCount)
3val editText: EditText = findViewById(R.id.editText)
4
5editText.addTextChangedListener(object : TextWatcher {
6    override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
7    override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
8
9    override fun afterTextChanged(s: Editable?) {
10        val remaining = maxChars - (s?.length ?: 0)
11        charCount.text = "$remaining characters remaining"
12        charCount.setTextColor(if (remaining < 0) Color.RED else Color.GRAY)
13    }
14})

Using Kotlin Extension (doAfterTextChanged)

kotlin
1// Requires: implementation 'androidx.core:core-ktx:1.9.0'
2import androidx.core.widget.doAfterTextChanged
3
4editText.doAfterTextChanged { text ->
5    val remaining = maxChars - (text?.length ?: 0)
6    charCount.text = "$remaining characters remaining"
7}

doAfterTextChanged is a Kotlin extension that eliminates the need to implement all three TextWatcher methods.

Enforcing Maximum Length

Using InputFilter (Preferred)

kotlin
import android.text.InputFilter

editText.filters = arrayOf(InputFilter.LengthFilter(280))

This prevents the user from typing beyond 280 characters. Combine with TextWatcher for the count display.

Using TextWatcher (Programmatic Trim)

java
1@Override
2public void afterTextChanged(Editable s) {
3    if (s.length() > MAX_CHARS) {
4        s.delete(MAX_CHARS, s.length());  // Trim excess
5    }
6    charCount.setText(s.length() + "/" + MAX_CHARS);
7}

Modifying s inside afterTextChanged triggers the TextWatcher again. The re-entry is safe because the trimmed text will pass the length check on the second call.

XML Layout

xml
1<LinearLayout
2    android:layout_width="match_parent"
3    android:layout_height="wrap_content"
4    android:orientation="vertical">
5
6    <EditText
7        android:id="@+id/editText"
8        android:layout_width="match_parent"
9        android:layout_height="wrap_content"
10        android:hint="Type your message..."
11        android:maxLength="280"
12        android:inputType="textMultiLine" />
13
14    <TextView
15        android:id="@+id/charCount"
16        android:layout_width="wrap_content"
17        android:layout_height="wrap_content"
18        android:layout_gravity="end"
19        android:text="280 characters remaining"
20        android:textSize="12sp"
21        android:textColor="@android:color/darker_gray" />
22</LinearLayout>

android:maxLength="280" in XML is equivalent to InputFilter.LengthFilter(280) in code.

Counting Words Instead of Characters

kotlin
1editText.doAfterTextChanged { text ->
2    val words = text.toString().trim().split("\\s+".toRegex())
3    val wordCount = if (text.isNullOrBlank()) 0 else words.size
4    wordCountView.text = "$wordCount words"
5}

Jetpack Compose Version

kotlin
1@Composable
2fun CharacterCountField(maxChars: Int = 280) {
3    var text by remember { mutableStateOf("") }
4
5    Column {
6        TextField(
7            value = text,
8            onValueChange = { if (it.length <= maxChars) text = it },
9            label = { Text("Message") }
10        )
11        Text(
12            text = "${maxChars - text.length} characters remaining",
13            color = if (text.length > maxChars - 20) Color.Red else Color.Gray,
14            fontSize = 12.sp
15        )
16    }
17}

In Compose, there is no TextWatcher — the onValueChange lambda handles all text changes.

Common Pitfalls

  • Counting in onTextChanged instead of afterTextChanged: onTextChanged fires during the edit, so the CharSequence may not reflect the final state if multiple watchers modify the text. Use afterTextChanged for accurate counts.
  • Infinite loop when modifying Editable: Calling s.replace() or s.delete() inside afterTextChanged re-triggers the watcher. Always guard modifications with a length check to prevent infinite recursion.
  • Forgetting to remove TextWatcher: If you add a watcher in onResume, remove it in onPause with removeTextChangedListener(). Otherwise, duplicate watchers accumulate on configuration changes.
  • Unicode multi-byte characters: s.length() counts UTF-16 code units, not user-visible characters. An emoji like a flag uses 4 code units but appears as 1 character. Use Character.codePointCount() for accurate visible character counts.
  • Using android:maxLength and programmatic trim together: If both are set, the InputFilter prevents the text from exceeding the limit, so the afterTextChanged trim never triggers. Use one or the other, not both.

Summary

  • Use afterTextChanged in TextWatcher to count characters after the edit is finalized
  • Display remaining characters with a TextView updated inside the callback
  • Use InputFilter.LengthFilter or android:maxLength to enforce limits at the input level
  • Kotlin's doAfterTextChanged extension simplifies the implementation
  • In Jetpack Compose, handle counting in the onValueChange lambda
  • Guard any text modifications inside afterTextChanged to avoid infinite re-triggering

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Interview Questions practice on Codemia

Over 8,000 real interview questions from top companies, searchable by company and role.

Browse interview questions