Intermediate: Android basics · Jetpack Compose UI · topic 26 of 50 · 30 min
Text input and forms
Explanation
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.
@Composable
fun NameField() {
var name by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Full name") },
singleLine = true,
)
}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
| Field | KeyboardOptions |
|---|---|
| Mobile number | keyboardType = KeyboardType.Phone |
| PIN code, OTP, amount | keyboardType = KeyboardType.Number (or NumberPassword for OTP) |
| keyboardType = KeyboardType.Email | |
| Password | keyboardType = KeyboardType.Password plus visualTransformation = PasswordVisualTransformation() |
| Last field of a form | imeAction = 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).
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,
)
}A complete small form
@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")
}
}
}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.
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,
)
}Practice (0/3 done)
A TextField shows the starting text but typing does nothing. What is the likely bug?
Which keyboard type suits a 10-digit mobile number?
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
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Layouts: Column, Row, Box and ScaffoldJetpack Compose UI · same module
- State and recompositionJetpack Compose UI · same module
- Forms and client-side validationFull-Stack Development · Building the frontend
- Forms with JavaScriptFrontend Web Development · JavaScript in the browser: DOM and events
- Forms and validationFlutter and Dart · Lists, navigation and forms
- Built-in form validationFrontend Web Development · Forms, semantic HTML and accessibility
Questions people ask about this
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.
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.
Which type of key does a Form use for validation?Related
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.
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
- This is the last topic of “Jetpack Compose UI”. The module exam comes next.
- Module 6 exam: Jetpack Compose UI · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
- Then the next module, Navigation and app architecture. 50 topics left in the course before the final exam.