This episode masters user interaction: TextField, Button, Checkbox, and Switch, gesture detection with pointer input, focus and keyboard management, input validation, and Snackbar, dialogs, and bottom sheets.

An application without interaction is just a digital poster. Episode 7 takes you from static to interactive: capturing user input, detecting gestures, managing the keyboard and focus, and giving feedback through Snackbar, dialogs, and bottom sheets.
The pattern used here is consistent with episode 5: state goes up, events go down. Every control receives a value and a callback, and logic stays in the hoisted state or the ViewModel.
Episode 7 covers input controls, gestures and pointer input, focus and keyboard, validation, and feedback components.
OutlinedTextField and TextField are Material 3 text inputs. Both accept value and onValueChange — the state hoisting pattern you already know:
var email by rememberSaveable { mutableStateOf("") }
OutlinedTextField(
value = email,
onValueChange = { email = it },
label = { Text("Email") },
singleLine = true,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = { verifikasi() })
)KeyboardOptions configures the keyboard type (KeyboardType.Email) and IME action (ImeAction.Done), while KeyboardActions catches when the Done button is pressed — a pattern that makes forms feel responsive.
For binary choices, Checkbox and Switch use a Boolean state:
var setuju by remember { mutableStateOf(false) }
var notifikasi by remember { mutableStateOf(true) }
Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = setuju, onCheckedChange = { setuju = it })
Text("Setuju dengan syarat")
}
Row(verticalAlignment = Alignment.CenterVertically) {
Switch(checked = notifikasi, onCheckedChange = { notifikasi = it })
Text("Aktifkan notifikasi")
}Most interactions are covered by the clickable, toggleable, or selectable modifiers. For richer gestures, use pointerInput with detectors:
Modifier
.pointerInput(Unit) {
detectTapGestures(
onTap = { onTampilkanToast() },
onLongPress = { onBukaMenu() }
)
}
.padding(16.dp)detectTapGestures distinguishes onTap from onLongPress. For drag and pinch, Compose provides detectDragGestures and detectTransformGestures — the foundation for components such as a swipeable carousel. When testing without touching the screen, adb shell input tap 540 1200 sends a tap at specific coordinates from the terminal.
Focus moves when the user presses the IME button. FocusRequester gives programmatic control:
val passwordFocus = FocusRequester()
OutlinedTextField(
value = password,
onValueChange = { password = it },
modifier = Modifier.focusRequester(passwordFocus)
)
LaunchedEffect(Unit) { passwordFocus.requestFocus() }Modifier.focusRequester(passwordFocus) registers the requester, and passwordFocus.requestFocus() moves the focus when composition starts — useful for showing the keyboard right away when a form screen opens.
Validation produces an error message shown via isError and supportingText:
val emailValid = email.contains("@")
OutlinedTextField(
value = email,
onValueChange = { email = it },
isError = !emailValid && email.isNotEmpty(),
supportingText = {
if (!emailValid && email.isNotEmpty()) {
Text("Format email tidak valid")
}
}
)isError marks the field red, and supportingText shows the message. For complex forms, create a separate validation state so the logic can be tested without the UI — a pattern we will use again in episode 14.
SnackbarHost and SnackbarHostState show short messages at the bottom of the screen:
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState) }
) { padding ->
Button(
onClick = {
scope.launch {
snackbarHostState.showSnackbar("Berhasil disimpan")
}
},
modifier = Modifier.padding(padding)
) { Text("Simpan") }
}snackbarHostState.showSnackbar(...) shows the message; the call is suspend, so it runs inside rememberCoroutineScope — a coroutine concept in Compose that will be dissected in episode 10.
For confirmation, use AlertDialog; for contextual actions, use ModalBottomSheet:
var tampilkanDialog by remember { mutableStateOf(false) }
if (tampilkanDialog) {
AlertDialog(
onDismissRequest = { tampilkanDialog = false },
title = { Text("Hapus item?") },
text = { Text("Tindakan ini tidak bisa dibatalkan.") },
confirmButton = {
Button(onClick = { tampilkanDialog = false }) { Text("Hapus") }
},
dismissButton = {
TextButton(onClick = { tampilkanDialog = false }) { Text("Batal") }
}
)
}AlertDialog appears only when tampilkanDialog is true. This if (state) { Component } pattern is a common Compose idiom for overlays — it will come up again when discussing animations in episode 12.
Episode 7 equips you for interaction: TextField with keyboard options and validation, Checkbox and Switch for binary choices, gestures with pointerInput, focus control with FocusRequester, and feedback through Snackbar, AlertDialog, and ModalBottomSheet.
Key takeaways:
In episode 8 we will discuss navigation and app architecture — Navigation Compose and the nav graph, passing arguments, deep links, Single Activity architecture, and modular routing patterns.