Phase 2 · Building Interactive UIsModule 10~30 min read

Handling User Input

Buttons, text fields, checkboxes, switches, and sliders — building interactive, validated forms.

What you'll learn

Apps are a conversation with the user, and input is how they talk back. Now that you understand state and hoisting, capturing input is straightforward: an input control shows a value and reports changes — you already know this shape. We'll cover every common control and finish with a validated login form.

By the end you'll be able to:

  • Handle taps with Button and its variants
  • Capture typing with TextField and OutlinedTextField
  • Use Checkbox, Switch, and Slider
  • Assemble a form with live validation and a disabled submit button

Buttons & clicks

A Button takes an onClick lambda and a content slot for its label. Compose ships several variants that express different emphasis — use a filled button for the main action, outlined or text buttons for secondary ones:

Buttons.kt
Button(onClick = { /* do something */ }) {
    Text("Save")
}

// Variants for different emphasis:
OutlinedButton(onClick = {}) { Text("Cancel") }   // medium
TextButton(onClick = {}) { Text("Learn more") }    // low
IconButton(onClick = {}) { Icon(Icons.Default.Favorite, contentDescription = "Like") }
9:41
SaveCancelLearn more
Button emphasis — how this renders on a device

Text fields

A text field is the canonical hoisted composable: give it value and onValueChange. Prefer OutlinedTextField for its clear border. The keyboardOptions parameter tailors the on-screen keyboard — an email keyboard, a number pad, and so on — a small touch that makes an app feel polished:

EmailField.kt
@Composable
fun EmailField() {
    var email by remember { mutableStateOf("") }
    OutlinedTextField(
        value = email,
        onValueChange = { email = it },
        label = { Text("Email") },
        singleLine = true,
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email),
    )
}

Watch out

In onValueChange, always store the new value Compose hands you (the it). Forget to update the state and the field appears frozen — you type but nothing shows, because the field only ever displays whatever is in value. It's the most common beginner text-field bug.

Checkbox, Switch & Slider

Boolean and numeric inputs follow the exact same pattern — a value plus a change callback:

Toggles.kt
var checked by remember { mutableStateOf(false) }
Checkbox(checked = checked, onCheckedChange = { checked = it })

var on by remember { mutableStateOf(true) }
Switch(checked = on, onCheckedChange = { on = it })

var volume by remember { mutableStateOf(0.5f) }
Slider(value = volume, onValueChange = { volume = it })   // 0f..1f
9:41
✓Subscribe to updates
Dark mode
Volume
Selection controls — how this renders on a device

Building a form

A form is just several inputs sharing one screen, plus some rules. The trick is derived state: compute validation directly from the input values. Because those values are Compose state, the derived flags recompute automatically on every keystroke — so the submit button enables itself the moment the form becomes valid, with no manual bookkeeping:

LoginForm.kt
@Composable
fun LoginForm() {
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }

    // Derived validation — recomputed automatically as inputs change.
    val emailValid = "@" in email && "." in email
    val canSubmit = emailValid && password.length >= 6

    Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
        OutlinedTextField(
            value = email,
            onValueChange = { email = it },
            label = { Text("Email") },
            isError = email.isNotEmpty() && !emailValid,
            singleLine = true,
        )
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            singleLine = true,
        )
        Button(onClick = { /* submit */ }, enabled = canSubmit) {
            Text("Log in")
        }
    }
}

Tip

Disable the submit button (enabled = canSubmit) rather than letting a user submit garbage and then scolding them. Combined with isError on the field, the UI guides people toward a valid entry instead of punishing mistakes.
9:41
ada@example.com
••••••••
Log in
LoginForm() — how this renders on a device

Recap & quick check

Key takeaways

  • Button takes onClick and a content slot; use filled/outlined/text variants for different emphasis.
  • OutlinedTextField takes value + onValueChange — always store the new value in the callback.
  • keyboardOptions tailors the on-screen keyboard (email, number, etc.).
  • Checkbox, Switch, and Slider all follow the value + onChange pattern.
  • Compute validation as derived state from the inputs; use it to enable the submit button and show errors.

Quick check

1. You type in an OutlinedTextField but nothing appears. Most likely cause?

2. Which button variant signals the primary, high-emphasis action?

3. How do you show an email keyboard for a field?

4. A Slider's value uses what range by default?

5. Best way to stop invalid form submission?

You can capture any input now. Next we tackle showing lots of data efficiently with LazyColumn and LazyRow — the right way to build scrolling lists.