Skip to main content

Creating Forms and Validation

1. Using Form Widget

Property Values Description
autovalidateMode AutovalidateMode.disabled, AutovalidateMode.always, AutovalidateMode.onUserInteraction Controls when form validation occurs
child Widget The form's child widget, typically containing input fields
onChanged VoidCallback Called when any form field changes
onWillPop WillPopCallback Called when the user tries to pop the current screen

Example: Basic Form Setup

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Form Example')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                decoration: InputDecoration(labelText: 'Name'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your name';
                  }
                  return null;
                },
              ),
              TextFormField(
                decoration: InputDecoration(labelText: 'Email'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your email';
                  }
                  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                    return 'Please enter a valid email';
                  }
                  return null;
                },
              ),
              SizedBox(height: 20),
              ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                    // Process form data
                  }
                },
                child: Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Note: The GlobalKey<FormState> is essential for accessing form methods like validate() and save().
Warning: Never use TextFormField without a validator if the field is required. Always provide appropriate validation feedback to users.

2. Implementing Form Validation

Property Description Values
autovalidateMode Controls when the form validates its fields AutovalidateMode.disabled, AutovalidateMode.always, AutovalidateMode.onUserInteraction
child The form widget to be validated Widget

Example: Basic Form Validation

Form(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  child: Column(
    children: [
      TextFormField(
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'Please enter some text';
          }
          return null;
        },
      ),
    ],
  ),
)

Example: Custom Validation Logic

TextFormField(
  validator: (value) {
    if (value == null || value.length < 5) {
      return 'Must be at least 5 characters';
    }
    return null;
  },
)

Example: Email Validation

TextFormField(
  validator: (value) {
    if (value == null || !RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
      return 'Please enter a valid email';
    }
    return null;
  },
)
Note: Use TextFormField with a validator function to implement validation logic.
Warning: Validation only occurs when the form is submitted or when autovalidateMode triggers.

3. Saving Form Data

Example: Saving to Local Storage

// Save form data to local storage
import 'package:shared_preferences/shared_preferences.dart';

Future<void> saveFormData(Map<String, dynamic> formData) async {
  final prefs = await SharedPreferences.getInstance();
  prefs.setString('formData', jsonEncode(formData));
}
Property Description Usage
formData Map containing form field values Used for persistence and submission
SharedPreferences Local storage mechanism for Flutter Stores data persistently on device
Note: Always validate form data before saving to avoid invalid entries.
Warning: Sensitive data should not be stored in SharedPreferences without encryption.

4. Submitting Forms

Property Description Usage
onSubmit Callback function triggered when the form is submitted onSubmit: (formState) => print('Form submitted')
formKey Unique key to identify the form for validation and submission final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

Example: Basic Form Submission

class MyForm extends StatelessWidget {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Form Example')),
      body: Padding(
        padding: EdgeInsets.all(16.0),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                decoration: InputDecoration(labelText: 'Name'),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your name';
                  }
                  return null;
                },
              ),
              ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                    print('Form submitted');
                  }
                },
                child: Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Note: The validate() method must be called on the form's state to ensure all validators pass before submission.
Warning: Never submit sensitive data directly in the UI without proper backend handling and security measures.

5. Using AutovalidateMode

Property Values Description
autovalidateMode AutovalidateMode.disabled, AutovalidateMode.always, AutovalidateMode.onUserInteraction Determines when form validation occurs

Example: Setting AutovalidateMode

Form(
  autovalidateMode: AutovalidateMode.always,
  child: TextFormField(
    validator: (value) {
      if (value == null || value.isEmpty) {
        return 'Enter a value';
      }
      return null;
    },
  ),
)
Note: AutovalidateMode.always triggers validation on every change, while onUserInteraction only validates after the user interacts with the field.

6. Creating Custom Validators

Custom validators in Flutter allow you to define your own validation logic for form fields. They are particularly useful when the built-in validators do not meet your specific requirements.

Property Description Example
Validator Function Function that takes a String and returns a String? (error message) or null
String? validateName(String? value) {
  if (value == null || value.isEmpty) {
    return 'Name is required';
  }
  return null;
}

Example: Custom Email Validator

String? validateEmail(String? value) {
  if (value == null || value.isEmpty) {
    return 'Email is required';
  }
  final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}

Example: Using Custom Validator in Form Field

TextFormField(
  validator: validateEmail,
  decoration: InputDecoration(
    labelText: 'Email',
    border: OutlineInputBorder(),
  ),
)
Note: Custom validators are applied in the same way as built-in validators. The validator function is called when the form field is validated, and it should return null if the value is valid or a string error message otherwise.

Key Points

  • Custom validators are functions that take a value and return an error message or null
  • They provide flexibility to define complex validation logic
  • Validators are used in TextFormField or similar widgets with the validator property

7. Implementing Cross-Field Validation

Example: Cross-Field Password Confirmation

class PasswordForm extends StatefulWidget {
  @override
  _PasswordFormState createState() => _PasswordFormState();
}

class _PasswordFormState extends State<PasswordForm> {
  final _formKey = GlobalKey<FormState>();
  final _passwordController = TextEditingController();
  final _confirmPasswordController = TextEditingController();

  bool _isPasswordValid(String password) {
    return password.length >= 8 && RegExp(r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)').hasMatch(password);
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _passwordController,
            decoration: InputDecoration(labelText: 'Password'),
            validator: (value) {
              if (value == null || value.isEmpty) return 'Password is required';
              if (!_isPasswordValid(value)) return 'Must be at least 8 chars with uppercase, lowercase, and digit';
              return null;
            },
          ),
          TextFormField(
            controller: _confirmPasswordController,
            decoration: InputDecoration(labelText: 'Confirm Password'),
            validator: (value) {
              if (value == null || value.isEmpty) return 'Confirmation is required';
              if (value != _passwordController.text) return 'Passwords do not match';
              return null;
            },
          ),
        ],
      ),
    );
  }
}
Field Validation Type Description Notes
password Length + Pattern Minimum 8 characters with at least one uppercase, lowercase, and digit Uses regex pattern for validation
confirmPassword Equality Check Must match the password field exactly Validated against _passwordController.text
Note: Cross-field validation requires access to multiple form fields' values, typically achieved using controllers or a state management solution.

8. Using FormField Widget

The FormField widget in Flutter is a powerful component that wraps a form field and provides built-in validation capabilities. It allows you to define custom validation logic and integrates seamlessly with the Form widget.

Property Description Values
builder Required callback that builds the form field widget. (FormFieldState<T> state) => Widget
validator Function to validate the field's value. (T?) => String?
initialValue Initial value for the form field. T?
onSaved Callback to save the field's value when the form is saved. (T?) => void
onChanged Callback to handle changes in the field's value. (T?) => void
autovalidateMode Controls when the field is auto-validated. AutovalidateMode

Example: Using FormField with TextFormField

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<Form>();
  String? _email;

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          FormField<String>(
            builder: (FormFieldState<String> state) {
              return TextFormField(
                decoration: InputDecoration(
                  labelText: 'Email',
                  errorText: state.errorText,
                ),
                onChanged: (value) {
                  state.didChange(value);
                },
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Please enter your email';
                  }
                  if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
                    return 'Please enter a valid email';
                  }
                  return null;
                },
              );
            },
          ),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                print('Form is valid');
              }
            },
            child: Text('Submit'),
          ),
        ],
      ),
    );
  }
}
Note: The FormField widget does not provide a built-in UI for the field. You must define the UI using the builder callback.

Key Features of FormField

  • Supports custom validation logic.
  • Integrates with the Form widget for validation and saving.
  • Allows access to the FormFieldState for managing field state.
  • Enables custom UI rendering through the builder callback.

9. Resetting Forms

Property Description Usage
FormState.reset() Resets all form fields to their initial values Call on a FormState instance to clear all fields

Example: Resetting a Form

final formKey = GlobalKey<FormState>();

// Reset the form
formKey.currentState!.reset();
Note: The reset() method only resets the form fields to their initial values. It does not reset any validation or error states.

10. Managing Form State and Focus Navigation

Property Description Values
autovalidateMode Controls when the form validates its fields AutovalidateMode.disabled, AutovalidateMode.always, AutovalidateMode.onUserInteraction
child The form's child widget Widget
onChanged Callback when any field in the form changes VoidCallback
onSaved Callback when the form is saved VoidCallback
validator Function to validate the form's value FormValidator

Example: Form with Validation

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final GlobalKey<FormState> _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      autovalidateMode: AutovalidateMode.onUserInteraction,
      child: Column(
        children: [
          TextFormField(
            decoration: InputDecoration(labelText: 'Name'),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter a name';
              }
              return null;
            },
          ),
          TextFormField(
            decoration: InputDecoration(labelText: 'Email'),
            validator: (value) {
              if (value == null || !value.contains('@')) {
                return 'Please enter a valid email';
              }
              return null;
            },
          ),
        ],
      ),
    );
  }
}

Example: Focus Navigation

class FocusForm extends StatefulWidget {
  @override
  _FocusFormState createState() => _FocusFormState();
}

class _FocusFormState extends State<FocusForm> {
  final TextEditingController _nameController = TextEditingController();
  final TextEditingController _emailController = TextEditingController();

  FocusNode _nameFocusNode = FocusNode();
  FocusNode _emailFocusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextFormField(
          controller: _nameController,
          focusNode: _nameFocusNode,
          decoration: InputDecoration(labelText: 'Name'),
        ),
        TextFormField(
          controller: _emailController,
          focusNode: _emailFocusNode,
          decoration: InputDecoration(labelText: 'Email'),
        ),
        ElevatedButton(
          onPressed: () {
            if (_nameFocusNode.hasFocus) {
              FocusScope.of(context).requestFocus(_emailFocusNode);
            } else if (_emailFocusNode.hasFocus) {
              FocusScope.of(context).unfocus();
            }
          },
          child: Text('Next Field'),
        ),
      ],
    );
  }
}

11. Creating Reusable Form Components

Example: Custom Text Input Field

class CustomTextInput extends StatelessWidget {
  final String label;
  final TextEditingController controller;
  final String? Function(String?)? validator;

  const CustomTextInput({
    Key? key,
    required this.label,
    required this.controller,
    this.validator,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label),
        TextFormField(
          controller: controller,
          validator: validator,
          decoration: InputDecoration(
            border: OutlineInputBorder(),
          ),
        ),
      ],
    );
  }
}
Property Description Values
label Label text for the input field String
controller Controller to manage the input value TextEditingController
validator Validation function for the input field String? Function(String?)?
Note: The validator function should return null if the input is valid, or an error message string if it's invalid.

Example: Reusable Form Field with Validation

class ValidatedFormField extends StatelessWidget {
  final String label;
  final TextEditingController controller;
  final String? Function(String?) validator;

  const ValidatedFormField({
    Key? key,
    required this.label,
    required this.controller,
    required this.validator,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label),
        TextFormField(
          controller: controller,
          validator: validator,
          decoration: InputDecoration(
            border: OutlineInputBorder(),
            errorText: null, // Error handling is managed by Form
          ),
        ),
      ],
    );
  }
}
); if (!emailRegex.hasMatch(value)) { return 'Please enter a valid email'; } return null; }

Was this article helpful?