CPS251 Android Development by Scott Shaper

Managing Input State

Introduction

Think of input state management like having a conversation with your app - it needs to listen, understand, and respond to what users type in real-time. In this chapter, we'll explore how to handle text input in Compose, from simple text fields to complex forms with validation. We'll learn how to create responsive, user-friendly interfaces that give immediate feedback to users as they type.

Quick Reference: Input State Patterns

Pattern Description When to Use
Basic Text Input Simple text field with state management Single input fields, search boxes
Form Validation Real-time input validation with feedback Login forms, registration forms
Debounced Input Delayed processing of input changes Search fields, auto-save features
State Hoisting Managing state at parent level Complex forms, shared input state

Basic Input State Management

Basic input state management is the foundation of handling user input in Compose. Think of it like having a notepad that automatically updates whenever someone writes something new. It's a way to store, track, and update what users type in your app's text fields. The state persists across UI updates, ensuring that user input isn't lost when the screen refreshes or changes.

When to Use Basic Input State

Common Input State Features

Feature What It Does When to Use It
mutableStateOf Creates state that can be updated Any time you need to store user input
OutlinedTextField Material Design text input field Most text input scenarios
VisualTransformation Changes how text appears Password fields, formatted input

Real-World Example: A Login Screen

Let's look at a complete example that shows these special input state features:

NOTE: In order for the icon on the password field to work, you need to add the following to your libs.versions.toml file:

[libraries]
compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" }

You also need to add the following to your build.gradle file:

implementation(libs.compose.material.icons.extended)
@Composable
fun LoginScreen() {
    // State for username and password
    var username by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    // State to remember if we're showing the password
    var showPassword by remember { mutableStateOf(false) }
    // State for validation
    var isUsernameValid by remember { mutableStateOf(true) }
    var isPasswordValid by remember { mutableStateOf(true) }

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Username field with validation
        OutlinedTextField(
            value = username,
            onValueChange = { 
                username = it
                isUsernameValid = it.length >= 3
            },
            label = { Text("Username") },
            modifier = Modifier.fillMaxWidth(),
            isError = !isUsernameValid && username.isNotEmpty(),
            supportingText = {
                if (!isUsernameValid && username.isNotEmpty()) {
                    Text("Username must be at least 3 characters")
                }
            }
        )

        // Password field with show/hide and validation
        OutlinedTextField(
            value = password,
            onValueChange = { 
                password = it
                isPasswordValid = it.length >= 6
            },
            label = { Text("Password") },
            modifier = Modifier.fillMaxWidth(),
            visualTransformation = if (showPassword) VisualTransformation.None else PasswordVisualTransformation(),
            isError = !isPasswordValid && password.isNotEmpty(),
            supportingText = {
                if (!isPasswordValid && password.isNotEmpty()) {
                    Text("Password must be at least 6 characters")
                }
            },
            trailingIcon = {
                IconButton(onClick = { showPassword = !showPassword }) {
                    Icon(
                        if (showPassword) Icons.Default.VisibilityOff else Icons.Default.Visibility,
                        contentDescription = if (showPassword) "Hide password" else "Show password"
                    )
                }
            }
        )

        // Login button (disabled if inputs are invalid)
        Button(
            onClick = { /* Handle login */ },
            modifier = Modifier.fillMaxWidth(),
            enabled = isUsernameValid && isPasswordValid && username.isNotEmpty() && password.isNotEmpty()
        ) {
            Text("Login")
        }
    }
}

Code Explanation:

State Variables:

Layout (Column):

The main content of the login screen is arranged vertically using a Column:

Username OutlinedTextField:

This input field handles the username entry and its validation:

Password OutlinedTextField:

This input field handles password entry, its validation, and the show/hide functionality:

Login Button:

The button to initiate the login process:

How these examples render

The images below show the login screen at different stages. The snippets above are only part of the code; to see and run the full project, go to my GitHub page and open the chapter7 inputstate.kt file.

Input State Example 1 Input State Example 2 Input State Example 3

State Hoisting in Input Management

State hoisting is a pattern in Compose where you move state management up to a parent component, making it the single source of truth for that state. Think of it like a family tree: instead of each child component managing its own state, the parent component holds the state and passes it down to its children. This is particularly useful for input management when you need to share state between multiple components or when child components need to communicate with each other.

When to Use State Hoisting

Here's a simple example of state hoisting with an input field:

@Composable
fun ParentScreen() {
    // State is hoisted to the parent
    var text by remember { mutableStateOf("") }
    
    Column {
        // Pass state and update function to child
        CustomTextField(
            value = text,
            onValueChange = { text = it },
            label = "Enter text"
        )
        
        // Another component using the same state
        Text("You typed: $text")
    }
}

@Composable
fun CustomTextField(
    value: String,
    onValueChange: (String) -> Unit,
    label: String
) {
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        label = { Text(label) }
    )
}

What This Example Is Doing

ParentScreen holds the only state: text. It passes value = text and onValueChange = { text = it } to CustomTextField, so the child never owns state—it just displays and reports changes. The parent also shows "You typed: $text" below the field, so the same state drives both the field and the label. CustomTextField is stateless and reusable: it only needs a value and a callback.

Let's break down how this hoisting example works:

State Management:

Benefits of Hoisting:

Here's a more practical example showing hoisting with validation:

@Composable
fun FormScreen() {
    // State Hoisting: All state is managed at the parent level
    // This allows child components to share and react to state changes
    var name by remember { mutableStateOf("") }
    var email by remember { mutableStateOf("") }
    // This state depends on both name and email, showing why hoisting is useful
    var isFormValid by remember { mutableStateOf(false) }
    
    Column {
        // State Hoisting: Passing state and update functions to child components
        // The child components don't manage their own state, they just display and update it
        NameInput(
            value = name,
            onValueChange = { 
                name = it
                // Validation happens at the parent level, affecting multiple components
                isFormValid = name.isNotEmpty() && email.isNotEmpty()
            }
        )
        
        EmailInput(
            value = email,
            onValueChange = { 
                email = it
                // Same validation logic is shared between components
                isFormValid = name.isNotEmpty() && email.isNotEmpty()
            }
        )
        
        // The button's state depends on the hoisted validation state
        // This wouldn't be possible if each input managed its own state
        Button(
            onClick = { /* Handle submit */ },
            enabled = isFormValid
        ) {
            Text("Submit")
        }
    }
}

@Composable
fun NameInput(
    value: String,
    onValueChange: (String) -> Unit
) {
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        label = { Text("Name") }
    )
}

@Composable
fun EmailInput(
    value: String,
    onValueChange: (String) -> Unit
) {
    OutlinedTextField(
        value = value,
        onValueChange = onValueChange,
        label = { Text("Email") }
    )
}

Code Explanation:

This Kotlin Composable function, FormScreen, along with its child components NameInput and EmailInput, demonstrates a fundamental concept in Jetpack Compose called **State Hoisting**. State hoisting involves moving the state management (e.g., mutableStateOf variables) from a child composable to its parent, making the child composable "stateless."

FormScreen Composable: (Parent Component - State Holder)

This composable is responsible for holding and managing the state for the form:

Inside the Column layout:

NameInput Composable: (Child Component - Stateless)

This composable is a reusable input field that doesn't manage its own state:

EmailInput Composable: (Child Component - Stateless)

Similar to NameInput, this is another stateless child component for the email input, showcasing the reusability and simplified logic that state hoisting enables.

How these examples render

The images below show the state-hoisting form. The snippets above are only part of the code; to see and run the full project, go to my GitHub page and open the chapter7 hoisting.kt file.

State Hoisting Example 1 State Hoisting Example 2

Tips for Success

Common Mistakes to Avoid

Best Practices