Part of Github Copilot with Kotlin

Jetpack Compose with Kotlin: Create a composable function with a textfield for name entry

Sandy LaneSandy Lane

Video: Jetpack Compose with Kotlin: Create a composable function with a textfield for name entry by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Jetpack Compose: TextField for Name Entry

var name by remember { mutableStateOf("") }. TextField(value = name, onValueChange = { name = it }). The textfield + live preview is the canonical "user input" Compose pattern.

A small puzzle: a text input that updates a label as the user types. Demos Compose's two-way binding pattern via value + onValueChange.

The Copilot prompt

import androidx.compose.runtime.Composable

// Composable with a TextField for name entry, showing the typed value below
@Composable
fun NameEntryScreen() {

Copilot generates:

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Surface
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun NameEntryScreen() {
  var name by remember { mutableStateOf("") }

  Surface(modifier = Modifier.padding(16.dp)) {
    Column {
      TextField(
        value = name,
        onValueChange = { name = it },
        label = { Text("Enter your name") },
      )
      Text(
        text = "Hello, $name!",
        style = MaterialTheme.typography.headlineSmall,
        modifier = Modifier.padding(top = 16.dp),
      )
    }
  }
}

Walkthrough

1. var name by remember { mutableStateOf("") }. Local state. Each character typed updates name; Compose recomposes any reader.

2. TextField(value, onValueChange, label). The state-driven input. - value = name — the current text the field displays. - onValueChange = { name = it } — what to do with new input. Lambda receives the new String; we update our state. - label = { Text("Enter your name") } — floating label, animates above the field when focused or non-empty.

3. Text("Hello, $name!"). Reads name. Updates on every keystroke.

That's it. Two-way binding without two-way binding — Compose's unidirectional data flow plus the value + onValueChange pattern.

TextField variants

Material 3 has three flavors:

  • TextField — filled background, line under the input. The default Material style.
  • OutlinedTextField — bordered, no fill. More common in modern apps.
  • BasicTextField — no decoration at all. Use when you want full control over styling.
OutlinedTextField(
  value = name,
  onValueChange = { name = it },
  label = { Text("Name") },
  placeholder = { Text("e.g., Alice") },
  singleLine = true,
)

placeholder shows greyed text when empty. singleLine = true prevents Enter from inserting a newline (common for "name" type fields).

Validation

var name by remember { mutableStateOf("") }
val isValid = name.isNotBlank() && name.length >= 2

OutlinedTextField(
  value = name,
  onValueChange = { name = it },
  label = { Text("Name") },
  isError = !isValid,
  supportingText = {
    if (!isValid) Text("Name must be at least 2 characters")
  },
)

isError = true makes the field red. supportingText slot shows beneath the field. Combined, the user gets immediate feedback.

Keyboard options

import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType

OutlinedTextField(
  value = name,
  onValueChange = { name = it },
  label = { Text("Name") },
  keyboardOptions = KeyboardOptions(
    capitalization = KeyboardCapitalization.Words,
    keyboardType = KeyboardType.Text,
    imeAction = ImeAction.Next,
  ),
  keyboardActions = KeyboardActions(
    onNext = { focusManager.moveFocus(FocusDirection.Down) },
  ),
)

KeyboardOptions shapes the on-screen keyboard. KeyboardActions handles the IME action (Next, Done, Search, etc.).

For a phone field: KeyboardType.Phone. For email: KeyboardType.Email. Wraps appropriately on Android.

Password fields

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation

var password by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }

OutlinedTextField(
  value = password,
  onValueChange = { password = it },
  label = { Text("Password") },
  visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
  trailingIcon = {
    IconButton(onClick = { passwordVisible = !passwordVisible }) {
      Icon(
        if (passwordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility,
        contentDescription = if (passwordVisible) "Hide password" else "Show password",
      )
    }
  },
)

PasswordVisualTransformation() masks the input as dots. Toggle with VisualTransformation.None to reveal.

Hoisting state

For a real form, hoist state to a ViewModel:

class FormViewModel : ViewModel() {
  private val _name = MutableStateFlow("")
  val name: StateFlow<String> = _name.asStateFlow()

  fun onNameChange(value: String) {
    _name.value = value
  }
}

@Composable
fun NameEntryScreen(vm: FormViewModel = viewModel()) {
  val name by vm.name.collectAsState()
  TextField(
    value = name,
    onValueChange = vm::onNameChange,
    label = { Text("Name") },
  )
}

Now form state survives config changes and is testable without UI.

Common mistakes

Forgetting var in var name by remember. Using val makes the binding read-only — onValueChange = { name = it } errors.

Forgetting getValue/setValue imports. by delegation needs both. Compiler error otherwise.

Updating in a side-effect. onValueChange = { name = it; sendToServer() } fires on every keystroke. Debounce or move heavy work elsewhere (LaunchedEffect, button click).

Wrong KeyboardType. A phone-number field with KeyboardType.Text lets users enter letters. Use Phone.

Single-source confusion. If you keep both var name and var formState, they can drift out of sync. Pick one source of truth.

What's next

Episode 23: A counter button — increments on each click. Add count state + display + increment.

Recap

var text by remember { mutableStateOf("") } for local form state. TextField(value, onValueChange, label) is the standard pattern. OutlinedTextField for borders, BasicTextField for fully custom styling. Validate via isError = !isValid + supportingText. Tune the keyboard with KeyboardOptions. For real forms, hoist to a ViewModel with MutableStateFlow. Mask passwords with PasswordVisualTransformation().

Next episode: counter button.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.