Guide to forms in Flutter

Guide to forms in Flutter

Introduction

Applications frequently require data input from users, such as when creating an account. Forms serve as the primary tool for this purpose, making it crucial to understand the principles of effective form design and implementation.

  1. Form Fundamentals
  2. Validation
    1. Async Validation
  3. Dynamic form fields
  4. Multi-page form
  5. State Restoration

Form Fundamentals

A form consists of multiple inputs designed to collect structured data from users. This data can then be processed and utilized within the application.

Forms typically necessitate input validation to ensure the data is valid before submission. This process involves retrieving values from inputs and verifying their adherence to specific rules. For instance, a password validator may check for the presence of special characters.

Moreover, forms can be complex, involving multiple pages, attachments, and dynamic fields. In such cases, it is crucial to address various concerns, including state preservation, delivering a positive user experience (UX), and effective error handling.

To begin, we will create a basic sign-up form featuring username and password fields, as follows:

Column(
  mainAxisSize: MainAxisSize.min,
  children: [
    const Text(
      'Sign Up',
      style: TextStyle(
        fontSize: 24,
        fontWeight: FontWeight.w600,
      ),
    ),
    Padding(
      padding: const EdgeInsets.only(top: 16),
      child: TextField(
        style: Theme.of(context).textTheme.bodyMedium,
        decoration: const InputDecoration(
          labelText: 'Username',
          border: OutlineInputBorder(),
        ),
      ),
    ),
    Padding(
      padding: const EdgeInsets.only(top: 16),
      child: TextField(
        style: Theme.of(context).textTheme.bodyMedium,
        decoration: const InputDecoration(
          labelText: 'Password',
          border: OutlineInputBorder(),
        ),
      ),
    ),
    Padding(
      padding: const EdgeInsets.only(top: 16),
      child: FilledButton.icon(
        onPressed: submitForm,
        icon: const Icon(Icons.send),
        label: const Text('Send'),
      ),
    ),
  ],
)

Currently, this is a simple screen with two text fields, lacking any validation.

Validation

To enhance the sign-up form with validation functionality, we will switch from using TextField widgets to TextFormField widgets. TextFormField widgets offer the advantage of built-in validation capabilities, working seamlessly within a Form widget. This setup allows for individual field validation as well as collective form validation.

For username and password fields we will implement the following validation rules:

  1. Username Validation:
    • Rule 1: The username must be at least 6 characters long.
    • Rule 2: The username must consist only of alphanumeric characters, which includes Latin letters and numbers.
  2. Password Validation:
    • The password must be at least 8 characters long.

Here's an example implementation that incorporates these validation rules:

Form(
  child: Builder(builder: (context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text(
          'Sign Up',
          style: TextStyle(
            fontSize: 24,
            fontWeight: FontWeight.w600,
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 16),
          child: TextFormField(
            style: Theme.of(context).textTheme.bodyMedium,
            decoration: const InputDecoration(
              labelText: 'Username',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              value = value ?? '';
              if (value.length < 6) {
                return 'Username must be at least 6 characters long';
              }

              final regex = RegExp(r'^[a-zA-Z0-9]+$');

              if (!regex.hasMatch(value)) {
                return 'Username must only contain alphanumeric characters';
              }

              return null;
            },
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 16),
          child: TextFormField(
            style: Theme.of(context).textTheme.bodyMedium,
            decoration: const InputDecoration(
              labelText: 'Password',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              value = value ?? '';

              if (value.length < 8) {
                return 'Password must be at least 8 characters long';
              }

              return null ;
            },
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Text(
            'Username should be at least 6 characters long and only contain alphanumeric characters. '
            'Password should be at least 8 characters long.',
            style: Theme.of(context).textTheme.bodySmall?.copyWith(
                  color: Theme.of(context).colorScheme.outline,
                ),
          ),
        ),
        Padding(
          padding: const EdgeInsets.only(top: 16),
          child: FilledButton.icon(
            onPressed: Form.of(context).validate,
            icon: const Icon(Icons.send),
            label: const Text('Send'),
          ),
        ),
      ],
    );
  }),

Each field now includes a validator that verifies the input's validity. For instance, the username validator employs a regular expression to ascertain whether the input consists solely of alphanumeric characters. Conversely, the password validator adopts a simpler approach by merely assessing the string's length.

To centralize the validation process, the column is wrapped with a Form widget. Furthermore, a Builder is utilized to create a context that has an access to the inherited widget, as created by the Form, which the "Send" button later leverages to validate the fields. See the implementation in action:

0:00
/0:18

This method should adequately cover most situations. However, it's important to note that validators assigned to text form fields are restricted to synchronous operations. This limitation introduces the need for asynchronous validation to address scenarios where synchronous validators are insufficient.

Async Validation

Asynchronous validation is a process designed to verify inputs using a future-based API. This typically involves sending a request to a server or using some specific native functionalities, such as those available through Flutter's MethodChannel.

Read more