Skip to content

Intermediate · Lists, navigation and forms · topic 26 of 50 · 35 min

Forms and validation

Explanation

Wrap fields in a Form with a GlobalKey<FormState>. Each TextFormField has a validator that returns an error message or null. Calling _formKey.currentState!.validate() checks every field and shows errors. Use TextEditingController to read or set text (and dispose it), keyboardType for the right keyboard, obscureText for passwords, inputFormatters to restrict input, and autovalidateMode to re-check as the user types.

Why Form?

Sign-up, address, payment and feedback screens all collect input. You need to check it (validation), show clear errors next to the right field, and only submit when everything is correct. Flutter's Form widget groups fields so you can validate, save or reset them together.

dart
class SignupForm extends StatefulWidget {
  const SignupForm({super.key});
  @override
  State<SignupForm> createState() => _SignupFormState();
}

class _SignupFormState extends State<SignupForm> {
  final _formKey = GlobalKey<FormState>();
  final _name = TextEditingController();
  final _mobile = TextEditingController();

  @override
  void dispose() {
    _name.dispose();
    _mobile.dispose();
    super.dispose();
  }

  void _submit() {
    if (_formKey.currentState!.validate()) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Welcome, ${_name.text.trim()}!')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            controller: _name,
            decoration: const InputDecoration(labelText: 'Full name'),
            validator: (v) => (v == null || v.trim().length < 2) ? 'Enter your name' : null,
          ),
          TextFormField(
            controller: _mobile,
            decoration: const InputDecoration(labelText: 'Mobile number', prefixText: '+91 '),
            keyboardType: TextInputType.phone,
            inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(10)],
            validator: validateMobile,
          ),
          const SizedBox(height: 16),
          FilledButton(onPressed: _submit, child: const Text('Create account')),
        ],
      ),
    );
  }
}
inputFormatters come from package:flutter/services.dart.

Validators

A validator is a function that receives the current text and returns null if it is valid, or an error message string if not. Keep validators as small, plain functions so you can unit test them (module 10).

dart
String? validateMobile(String? value) {
  final v = value?.trim() ?? '';
  if (v.isEmpty) return 'Mobile number is required';
  if (v.length != 10) return 'Enter 10 digits';
  if (!RegExp(r'^[6-9]').hasMatch(v)) return 'Indian mobile numbers start with 6, 7, 8 or 9';
  return null;
}

String? validatePin(String? value) =>
    RegExp(r'^[1-9][0-9]{5}$').hasMatch(value ?? '') ? null : 'Enter a valid 6-digit PIN code';
Messages should say what to do, not just "Invalid".

Controllers

A TextEditingController holds a field's text: read it with controller.text, set it with controller.text = '...', clear it with clear(), and listen for changes with addListener. Controllers hold resources, so create them in the State (as fields or in initState) and dispose them in dispose.

You do not always need a controller. TextFormField also has onChanged (called on every keystroke) and onSaved (called by formKey.currentState!.save()), which are enough for many forms. Use a controller when you need to set the text from code, clear it, or read it outside the field.

Keyboard and input settings

SettingEffect
keyboardType: TextInputType.phone / number / emailAddressShows the right keyboard
obscureText: trueHides password characters
textInputAction: TextInputAction.next / doneThe keyboard's action button moves to the next field or finishes
inputFormattersBlocks unwanted characters or limits length as the user types
autovalidateMode: AutovalidateMode.onUserInteractionRe-validates a field after the user starts editing it

Remember to import package:flutter/services.dart for input formatters. Formatters stop bad input early; validators explain what is still wrong.

Good form UX

  • Show errors after the user has interacted or tried to submit, not on a fresh form.
  • Disable the submit button or show a progress indicator while submitting, to avoid double submissions.
  • Keep the form scrollable (SingleChildScrollView or ListView) so the keyboard does not hide fields on small phones.
  • Validate on the server too: anyone can bypass app checks.
Think of it like this: A Form is like a bank clerk checking a deposit slip: she reads each box (field), circles the wrong ones with a note ("account number must have 11 digits"), and only accepts the slip when every box is right.

Common mistakes

  • Forgetting the key on Form, so currentState is null.
  • Not disposing TextEditingControllers: memory leaks.
  • Validators that throw or return empty strings: return null for valid, a message for invalid.
  • A non-scrollable form that the keyboard covers on small phones.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Forms and validation · 1/6

Form(key: _formKey, child: ...)

Wrap fields in a Form with a global key of form state.

Prefer a video by a teacher? Find free videos on “flutter form validation textformfield globalkey controller input formatters” on YouTube ↗

In real life: A delivery address form

A grocery app's address form has name, mobile, PIN code and address line fields. Each has a clear validator; the PIN field accepts only digits and at most 6 of them; pressing Next on the keyboard moves to the next field. The Save button validates everything and only then saves the address, so the delivery partner never receives a 5-digit PIN.

dart
TextFormField(
  decoration: const InputDecoration(labelText: 'PIN code'),
  keyboardType: TextInputType.number,
  textInputAction: TextInputAction.next,
  inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(6)],
  autovalidateMode: AutovalidateMode.onUserInteraction,
  validator: validatePin,
)
Indian PIN codes have 6 digits and do not start with 0.

Practice (0/3 done)

Task 1 · Question

What should a validator return for valid input?

Task 2 · Question

_formKey.currentState is null when you call validate(). What is the likely cause?

Task 3 · Practice

Build a signup form with name, email, mobile, password and confirm password. Write each validator as a separate function and test them in main or unit tests: email must contain @ and a dot after it, mobile 10 digits starting with 6-9, password at least 8 characters with a number, confirm must match. Make the form scrollable and dispose all controllers.

Quick quiz

Question 1

1.Which type of key does a Form use for validation?

Question 2

2.Which setting hides password characters?

Question 3

3.Which formatter allows only digits?

Question 4

4.Why must the server validate input again?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which type of key does a Form use for validation?

Answer: GlobalKey<FormState>

GlobalKey gives access to the FormState.

Which setting hides password characters?

Answer: obscureText: true

obscureText shows dots.

Which formatter allows only digits?

Answer: FilteringTextInputFormatter.digitsOnly

From flutter/services.dart.

Why must the server validate input again?

Answer: App-side checks can be bypassed

Never trust only the client.

Why must the server validate input again?Related

Answer: Client-side checks can be bypassed

Never trust only the client.

From Text input and forms

The form already checks that the title is 1 to 100 characters. Why must the server check again?Related

Answer: Requests can be sent without the form, for example with curl or edited code

Browser checks can be skipped. Only the server check protects the data.

From Forms and client-side validation

Which attribute stops a form from being sent while a field is empty?Related

Answer: required

required makes an empty field invalid, so the browser stops the submit.

From Built-in form validation

Which attribute must a field have so that new FormData(form) includes it?Related

Answer: name

FormData uses the name attribute as the key. A field without a name is left out.

From Forms with JavaScript

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. This is the last topic of “Lists, navigation and forms”. The module exam comes next.
  2. Module 6 exam: Lists, navigation and forms · 13 questions · 25 min, without help. 5 topics of this module still to finish first.
  3. Then the next module, State management. 50 topics left in the course before the final exam.

← Previous: Routing with go_router