CPS251 Android Development by Scott Shaper

Validation

Introduction

When users enter information into your app, you want to make sure they're providing the right kind of data. This is called input validation, and it's like having a helpful assistant that checks if the information is correct before letting users proceed. Let's learn how to add these helpful checks to your Compose apps!

Quick Reference: Common Validation Types

Validation Type What It Checks When to Use It
Required Fields If a field is empty Essential information like name, email
Format Validation Correct pattern (email, phone) Structured data like emails, dates
Length Validation Minimum/maximum length Passwords, usernames, messages
Matching Validation If two fields match Password confirmation, email confirmation

Basic Validation Concepts

When to Use Validation

  • When collecting user information
  • For form submissions
  • When data needs to follow a specific format
  • For security-sensitive information
  • When data needs to be consistent

Common Validation Features

Feature What It Does When to Use It
isError Shows red border for invalid input When input doesn't meet requirements
supportingText Displays error messages To explain what's wrong
regex patterns Validates complex formats For email, phone, password validation

Basic Validation Example

Let's start with a simple example that shows how to validate a required field, we have seen this type of example before so this is just a review.

@Composable
fun NameInput() {
    // State to store the name and validation status
    var name by remember { mutableStateOf("") }
    var isNameValid by remember { mutableStateOf(true) }

    Column {
        OutlinedTextField(
            value = name,
            onValueChange = { 
                name = it
                // Check if the name is not empty
                isNameValid = it.isNotEmpty()
            },
            label = { Text("Name") },
            // Show red border if name is empty
            isError = !isNameValid && name.isNotEmpty(),
            // Show error message below the field
            supportingText = {
                if (!isNameValid && name.isNotEmpty()) {
                    Text("Please enter your name")
                }
            }
        )
    }
}

What This Example Is Doing

NameInput keeps name and isNameValid in state. On each change it sets isNameValid = it.isNotEmpty(), so the field is valid when it has at least one character. The error state and message are shown only when the field is invalid and non-empty (!isNameValid && name.isNotEmpty()), so an empty field at startup doesn’t show an error. When the user types and then clears the field, the red border and "Please enter your name" appear.

Advanced Validation Patterns

Email Validation

Email addresses have a specific format they need to follow. Let's create a more complex validation that checks for a proper email format using regex:

@Composable
fun EmailInput() {
    var email by remember { mutableStateOf("") }
    var isEmailValid by remember { mutableStateOf(true) }
    var errorMessage by remember { mutableStateOf("") }
    
    // Create regex pattern for email validation
    val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$".toRegex()

    Column {
        OutlinedTextField(
            value = email,
            onValueChange = { 
                email = it
                // Use regex to check email format
                isEmailValid = it.isEmpty() || it.matches(emailRegex)
                // Set error message only when we'll show it (not empty and invalid format)
                errorMessage = if (it.isNotEmpty() && !it.matches(emailRegex)) "Please enter a valid email address" else ""
            },
            label = { Text("Email") },
            isError = !isEmailValid && email.isNotEmpty(),
            supportingText = {
                if (!isEmailValid && email.isNotEmpty()) {
                    Text(errorMessage)
                }
            }
        )
    }
}

What This Example Is Doing

EmailInput keeps three things in state: email (what the user typed), isEmailValid (whether that text looks like a valid email), and errorMessage (the exact message to show under the field when something is wrong). Keeping errorMessage in state lets us pick the right message as the user types and then show it in supportingText.

Every time the user types, onValueChange runs. It (1) updates email with the new text, (2) sets isEmailValid to true if the field is empty or if the text matches the email regex, and (3) sets errorMessage to "Please enter a valid email address" only when the field is not empty and doesn't match the regex—otherwise "". That way we only store a message when we will actually show it. isError and supportingText are only used when !isEmailValid && email.isNotEmpty(), so the field does not show an error when it is empty—the screen does not load with a red field, and if the user clears the field the error goes away. The user only sees the red state and "Please enter a valid email address" when they have typed something that is invalid (e.g. missing @).

This example uses a regex to decide what counts as a valid email. The pattern ^[A-Za-z0-9+_.-]+@(.+)$ checks for:

  • Letters (uppercase or lowercase), numbers, and characters like +, _, ., and - before the @
  • An @ symbol
  • Something after the @ (the domain part)

Note: This is a very simple email check. In real apps you might use a stricter or more complex pattern to validate email addresses.

Password Validation

Passwords often need to meet specific security requirements. Here's an example that checks for password strength using regex:

@Composable
fun PasswordInput() {
    var password by remember { mutableStateOf("") }
    var isPasswordValid by remember { mutableStateOf(true) }
    var errorMessage by remember { mutableStateOf("") }
    
    // Create regex pattern for password validation
    val passwordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$".toRegex()

    Column {
        OutlinedTextField(
            value = password,
            onValueChange = { 
                password = it
                // Use regex to check password requirements
                isPasswordValid = it.isEmpty() || it.matches(passwordRegex)
                // Set error message only when we'll show it (not empty and invalid)
                errorMessage = if (it.isNotEmpty() && !it.matches(passwordRegex)) "Password must be at least 8 characters with uppercase, lowercase, and numbers" else ""
            },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            isError = !isPasswordValid && password.isNotEmpty(),
            supportingText = {
                if (!isPasswordValid && password.isNotEmpty()) {
                    Text(errorMessage)
                }
            }
        )
    }
}

What This Example Is Doing

PasswordInput keeps password, isPasswordValid, and errorMessage in state. The regex requires at least one digit, one lowercase letter, one uppercase letter, and length 8 or more. visualTransformation = PasswordVisualTransformation() masks the input. We set errorMessage only when the field is not empty and doesn't match the regex—otherwise ""—so we only store a message when we will show it. isError and supportingText are used only when !isPasswordValid && password.isNotEmpty(), so the field does not show an error when it is empty (the screen does not load with a red field, and clearing the field clears the error). The user only sees the red state and "Password must be at least 8 characters with uppercase, lowercase, and numbers" when they have typed something that fails the requirements.

This password validation uses regex to check for:

  • Minimum length of 8 characters
  • At least one uppercase letter
  • At least one lowercase letter
  • At least one number

Registration Form Example

Let's put it all together in a registration form that validates multiple fields using regex:

@Composable
fun RegistrationForm() {
    var name by remember { mutableStateOf("") }
    var email by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var confirmPassword by remember { mutableStateOf("") }
    
    var isNameValid by remember { mutableStateOf(true) }
    var isEmailValid by remember { mutableStateOf(true) }
    var isPasswordValid by remember { mutableStateOf(true) }
    var isConfirmPasswordValid by remember { mutableStateOf(true) }
    
    // Create regex patterns
    val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$".toRegex()
    val passwordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$".toRegex()
    val nameRegex = "^[A-Za-z\\s]{2,}$".toRegex() // At least 2 characters, letters and spaces only

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        // Name field
        OutlinedTextField(
            value = name,
            onValueChange = { 
                name = it
                isNameValid = it.isEmpty() || it.matches(nameRegex)
            },
            label = { Text("Name") },
            isError = !isNameValid && name.isNotEmpty(),
            supportingText = {
                if (!isNameValid && name.isNotEmpty()) {
                    Text("Name must be at least 2 characters and contain only letters and spaces")
                }
            }
        )

        // Email field
        OutlinedTextField(
            value = email,
            onValueChange = { 
                email = it
                isEmailValid = it.isEmpty() || it.matches(emailRegex)
            },
            label = { Text("Email") },
            isError = !isEmailValid && email.isNotEmpty(),
            supportingText = {
                if (!isEmailValid && email.isNotEmpty()) {
                    Text("Please enter a valid email address")
                }
            }
        )

        // Password field
        OutlinedTextField(
            value = password,
            onValueChange = { 
                password = it
                isPasswordValid = it.isEmpty() || it.matches(passwordRegex)
            },
            label = { Text("Password") },
            visualTransformation = PasswordVisualTransformation(),
            isError = !isPasswordValid && password.isNotEmpty(),
            supportingText = {
                if (!isPasswordValid && password.isNotEmpty()) {
                    Text("Password must be at least 8 characters with uppercase, lowercase, and numbers")
                }
            }
        )

        // Confirm password field
        OutlinedTextField(
            value = confirmPassword,
            onValueChange = { 
                confirmPassword = it
                isConfirmPasswordValid = it == password
            },
            label = { Text("Confirm Password") },
            visualTransformation = PasswordVisualTransformation(),
            isError = !isConfirmPasswordValid && confirmPassword.isNotEmpty(),
            supportingText = {
                if (!isConfirmPasswordValid && confirmPassword.isNotEmpty()) {
                    Text("Passwords don't match")
                }
            }
        )

        // Submit button
        Button(
            onClick = { /* Handle registration */ },
            modifier = Modifier.fillMaxWidth(),
            enabled = isNameValid && isEmailValid && isPasswordValid && 
                     isConfirmPasswordValid && name.isNotEmpty() && 
                     email.isNotEmpty() && password.isNotEmpty() && 
                     confirmPassword.isNotEmpty()
        ) {
            Text("Register")
        }
    }
}

Understanding the RegistrationForm Composable (Step-by-Step)

This Kotlin code defines a RegistrationForm composable using Jetpack Compose, which implements a user registration form with real-time input validation. Let's break down how it works:

The RegistrationForm() Composable - Central State Management

The RegistrationForm() function is the central point of this UI. It is responsible for:

  • Holding all form data (state): It declares variables for name, email, password, and confirmPassword. These are marked with remember { mutableStateOf("") }, meaning their values can change, and Compose will automatically recompose (update) the UI when they do.
  • Holding all validation states: Similarly, it declares boolean variables like isNameValid, isEmailValid, etc., initialized to true. These will track the validity of each input.
  • Defining validation rules (Regex): It sets up regular expressions (emailRegex, passwordRegex, nameRegex) that define the criteria for valid input for each field.
var name by remember { mutableStateOf("") }
var email by remember { mutableStateOf("") }
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }

var isNameValid by remember { mutableStateOf(true) }
var isEmailValid by remember { mutableStateOf(true) }
var isPasswordValid by remember { mutableStateOf(true) }
var isConfirmPasswordValid by remember { mutableStateOf(true) }

// Create regex patterns
val emailRegex = "^[A-Za-z0-9+_.-]+@(.+)$".toRegex()
val passwordRegex = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z]).{8,}$".toRegex()
val nameRegex = "^[A-Za-z\\s]{2,}$".toRegex() // At least 2 characters, letters and spaces only

Structuring the Layout

A Column composable is used to arrange the input fields and button vertically, with some padding and spacing:

Column(
    modifier = Modifier
        .fillMaxWidth()
        .padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
) {
    // Input fields and button go here
}

Individual Input Fields (OutlinedTextField)

Each input field (Name, Email, Password, Confirm Password) uses an OutlinedTextField composable. Let's look at the "Name field" as an example, as the others follow a similar pattern:

OutlinedTextField(
    value = name, // 1. Displays the current 'name' state
    onValueChange = { // 2. Lambda executed when user types
        name = it // Updates the 'name' state in RegistrationForm
        isNameValid = it.isEmpty() || it.matches(nameRegex) // Updates 'isNameValid' state
    },
    label = { Text("Name") }, // 3. Label for the input field
    isError = !isNameValid && name.isNotEmpty(), // 4. Controls error visual
    supportingText = { // 5. Displays error message if needed
        if (!isNameValid && name.isNotEmpty()) {
            Text("Name must be at least 2 characters and contain only letters and spaces")
        }
    }
)
  1. value = name: This ties the text field's displayed content directly to the name state variable from `RegistrationForm`. Whatever is in `name` will be shown here.
  2. onValueChange = { ... }: This is a **callback function** (a lambda) that gets triggered every time the user types a character.
    • name = it: The `it` refers to the new text entered by the user. This line updates the `name` state variable in the `RegistrationForm`. Because `name` is a mutable state, this will trigger a recomposition, updating the `OutlinedTextField` with the new text.
    • isNameValid = it.isEmpty() || it.matches(nameRegex): This line immediately re-evaluates the validity of the name based on the `nameRegex`. `it.isEmpty()` handles the case where the field is empty (no error shown). The `isNameValid` state is updated, which will affect the `isError` parameter.
  3. label = { Text("Name") }: This provides a floating label for the input field.
  4. isError = !isNameValid && name.isNotEmpty(): This is a crucial line for visual feedback.
    • If `isNameValid` is `false` (meaning the input doesn't match the regex) AND the `name` field is not empty, `isError` becomes `true`.
    • When `isError` is `true`, the `OutlinedTextField` automatically changes its visual style (e.g., border color turns red) to indicate an error.
  5. supportingText = { ... }: This is another lambda that provides a small helper text below the input.
    • It only displays the error message ("Name must be at least 2 characters...") when `isNameValid` is `false` AND the `name` field is not empty, matching the `isError` condition.

The "Email field" (`isEmailValid`, `emailRegex`), "Password field" (`isPasswordValid`, `passwordRegex`, `PasswordVisualTransformation` to hide text), and "Confirm password field" (`isConfirmPasswordValid`, checks `it == password`) follow the same pattern, each with its own specific validation logic and error messages.

The Submit Button

Finally, a `Button` is displayed at the bottom:

Button(
    onClick = { /* Handle registration */ },
    modifier = Modifier.fillMaxWidth(),
    enabled = isNameValid && isEmailValid && isPasswordValid &&
             isConfirmPasswordValid && name.isNotEmpty() &&
             email.isNotEmpty() && password.isNotEmpty() &&
             confirmPassword.isNotEmpty()
) {
    Text("Register")
}
  • onClick = { /* Handle registration */ }: This is the lambda that will be executed when the button is pressed. Currently, it's just a placeholder.
  • enabled = ...: This condition determines if the button is clickable. The button is only enabled (`true`) if:
    • ALL validation states (`isNameValid`, `isEmailValid`, `isPasswordValid`, `isConfirmPasswordValid`) are `true`.
    • AND ALL input fields (`name`, `email`, `password`, `confirmPassword`) are not empty.
    This ensures that the user can only attempt to register when all inputs are valid and present.

Overall Flow:

When the user interacts with any `OutlinedTextField`:

  1. The `onValueChange` lambda for that field is executed.
  2. The corresponding state variable (e.g., `name`) in `RegistrationForm` is updated.
  3. The corresponding validation state (e.g., `isNameValid`) in `RegistrationForm` is updated based on the regex.
  4. Because these are mutable states, Compose detects the changes and triggers a recomposition of `RegistrationForm` and its children.
  5. During recomposition, the `OutlinedTextField`s re-render, potentially showing error visuals and messages based on their updated `isError` and `supportingText` conditions.
  6. The `Button` also re-renders, and its `enabled` state is re-evaluated based on the current validity and emptiness of all fields.

This entire process provides a responsive, real-time feedback mechanism for form validation.

This registration form shows how to:

  • Use regex patterns for validating name, email, and password
  • Show different error messages for each field
  • Enable/disable the submit button based on all validations
  • Handle password confirmation
  • Provide clear visual feedback for each field
  • Coordinate validation across multiple fields
  • Manage complex button state logic

How these examples render

The images below show the registration form 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 validation.kt file.

Registration Form Registration Form Registration Form

Tips for Success

  • Validate input as the user types for immediate feedback
  • Use clear, specific error messages
  • Show validation errors only after user interaction
  • Keep validation rules consistent across your app
  • Test validation with various input scenarios

Common Mistakes to Avoid

  • Showing errors before user starts typing
  • Using vague error messages
  • Not validating on both client and server side
  • Making validation rules too strict
  • Not handling edge cases in validation

Best Practices

  • Use regex for complex format validation
  • Provide helpful error messages
  • Validate in real-time when possible
  • Keep validation logic separate from UI
  • Consider accessibility in error messages