Skip to content

Intermediate: Android basics · Jetpack Compose UI · topic 26 of 50 · 30 min

Text input and forms

Explanation

TextField and OutlinedTextField take a value and an onValueChange lambda, so the text lives in your state. KeyboardOptions choose the keyboard (Phone, Number, Email) and the action button; PasswordVisualTransformation hides passwords. Show validation errors with isError and supportingText, and enable the submit button only when every field is valid.

Text fields are controlled

In Compose, a text field does not keep its own text. You pass the current value, and it calls onValueChange with the new text each time the user types. You store that text in state. This "controlled" approach means your code always knows exactly what is in the field, which makes validation simple.

kotlin
@Composable
fun NameField() {
    var name by rememberSaveable { mutableStateOf("") }
    OutlinedTextField(
        value = name,
        onValueChange = { name = it },
        label = { Text("Full name") },
        singleLine = true,
    )
}
Without updating name in onValueChange, typing would appear to do nothing.

TextField has a filled background; OutlinedTextField has a border. Both work the same way. label shows a hint that moves above the text when the user types, placeholder shows grey example text in an empty field, and singleLine = true stops the field from growing to several lines, which suits names, numbers and emails.

Because the text lives in your state, you can also clean it as the user types: remove spaces from a phone number, change an email to lowercase, or limit the length. Whatever you store in onValueChange is what the field shows.

The right keyboard

FieldKeyboardOptions
Mobile numberkeyboardType = KeyboardType.Phone
PIN code, OTP, amountkeyboardType = KeyboardType.Number (or NumberPassword for OTP)
EmailkeyboardType = KeyboardType.Email
PasswordkeyboardType = KeyboardType.Password plus visualTransformation = PasswordVisualTransformation()
Last field of a formimeAction = ImeAction.Done (the keyboard shows a Done button)

Choosing the right keyboard is a small detail that users notice: nobody wants to switch to numbers manually to type a 10-digit mobile number.

Validation

Validation means checking that input makes sense before using it. Show errors next to the field, in plain words, and only after the user has typed something (showing red errors on an empty, untouched form feels rude).

kotlin
fun isValidMobile(s: String) = s.length == 10 && s.all { it.isDigit() } && s[0] in '6'..'9'

@Composable
fun MobileField(value: String, onChange: (String) -> Unit) {
    val showError = value.isNotEmpty() && !isValidMobile(value)
    OutlinedTextField(
        value = value,
        onValueChange = { onChange(it.filter { c -> c.isDigit() }.take(10)) },
        label = { Text("Mobile number") },
        prefix = { Text("+91 ") },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone),
        isError = showError,
        supportingText = { if (showError) Text("Enter a valid 10-digit mobile number") },
        singleLine = true,
    )
}
The field only accepts digits, at most 10. isError colours it red and supportingText explains why.

A complete small form

kotlin
@Composable
fun SignupForm(onSubmit: (String, String) -> Unit) {
    var mobile by rememberSaveable { mutableStateOf("") }
    var password by rememberSaveable { mutableStateOf("") }
    val canSubmit = isValidMobile(mobile) && password.length >= 8

    Column(Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
        MobileField(mobile) { mobile = it }
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password (8+ characters)") },
            visualTransformation = PasswordVisualTransformation(),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Done),
            singleLine = true,
        )
        Button(onClick = { onSubmit(mobile, password) }, enabled = canSubmit, modifier = Modifier.fillMaxWidth()) {
            Text("Create account")
        }
    }
}
The button is disabled until both fields are valid. The parent decides what submitting does.
Think of it like this: A good form is like a helpful bank clerk filling a deposit slip with you: she points to the exact box that is wrong ("account number must have 11 digits"), not just "form rejected", and she does not let you hand it over until it is complete.

Security notes

Never log passwords or OTPs with Log.d. Validation on the phone is for user convenience only; the server must validate again, because anyone can bypass an app. Do not store passwords in plain text on the device.

Common mistakes

  • Forgetting to update state in onValueChange: the field ignores typing.
  • Using remember instead of rememberSaveable: the half-filled form is lost on rotation.
  • Showing all errors before the user types anything.
  • Trusting client-side validation alone for security.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Text input and forms · 1/6

OutlinedTextField(value = name, onValueChange = { name = it })

Compose text fields are controlled. You pass the value and update it in on value change.

Prefer a video by a teacher? Find free videos on “jetpack compose textfield form validation keyboard options” on YouTube ↗

In real life: Address form for a delivery app

A grocery app's address form has name, mobile, a 6-digit PIN code and the address line. The PIN field uses a number keyboard and accepts only digits; when 6 digits are entered, the app looks up the city and fills it in. The Save button stays disabled until every field is valid, so the delivery team never receives an address with a 5-digit PIN.

kotlin
fun isValidPin(pin: String) = pin.length == 6 && pin.all { it.isDigit() } && pin[0] != '0'

@Composable
fun PinField(pin: String, onPin: (String) -> Unit) {
    OutlinedTextField(
        value = pin,
        onValueChange = { onPin(it.filter(Char::isDigit).take(6)) },
        label = { Text("PIN code") },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        isError = pin.length == 6 && !isValidPin(pin),
        singleLine = true,
    )
}
Indian PIN codes have 6 digits and do not start with 0.

Practice (0/3 done)

Task 1 · Question

A TextField shows the starting text but typing does nothing. What is the likely bug?

Task 2 · Question

Which keyboard type suits a 10-digit mobile number?

Task 3 · Practice

Build a login form with mobile number and a 6-digit OTP. The mobile field accepts only digits (max 10) and shows an error for invalid numbers; the OTP field uses KeyboardType.NumberPassword and accepts exactly 6 digits; the Verify button is enabled only when both are valid. Use rememberSaveable and rotate to check.

Quick quiz

Question 1

1.What does onValueChange receive?

Question 2

2.Which parameter hides the characters of a password?

Question 3

3.Why must the server validate input again?

Question 4

4.Which keeps a half-filled form when the phone is rotated?

Read more (free)

Before this, learn

This topic builds on these. If something here feels new, open them first.

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

What does onValueChange receive?

Answer: The new text the user typed

Store it in your state.

Which parameter hides the characters of a password?

Answer: visualTransformation = PasswordVisualTransformation()

It shows dots instead of letters.

Why must the server validate input again?

Answer: Client-side checks can be bypassed

Never trust only the client.

Which keeps a half-filled form when the phone is rotated?

Answer: rememberSaveable

rememberSaveable survives configuration changes.

What is a controlled input in React?Related

Answer: An input whose value comes from state and whose onChange updates that state

React state holds the value, so what you validate and send matches what is on screen.

From Forms and client-side validation

Why disable the submit button while the form is being sent?Related

Answer: So a double tap does not send the form twice

A disabled button cannot be pressed again, so users do not create two accounts or two bookings by accident.

From Forms with JavaScript

Which type of key does a Form use for validation?Related

Answer: GlobalKey<FormState>

GlobalKey gives access to the FormState.

From Forms and validation

Which attribute stops a form from being sent while a field is empty?Related

Answer: required

required makes an empty field invalid, so the browser stops the submit.

From Built-in form validation

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Jetpack Compose UI”. The module exam comes next.
  2. Module 6 exam: Jetpack Compose UI · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, Navigation and app architecture. 50 topics left in the course before the final exam.

← Previous: Lists and grids