flutter
/

Flutter Form Widget Tutorial for Beginners

Last Sync: Today

On this page

10
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Flutter Form Widget Tutorial for Beginners

What is Form in Flutter?

Form is a widget that acts as a container for grouping and validating multiple form fields (like TextFormField, DropdownButtonFormField, etc.). It provides a way to save, reset, or validate all the fields at once using a GlobalKey<FormState>. Forms are essential for handling user input in a structured and maintainable way.

Basic Usage

To create a form, wrap your input fields in a Form widget and give it a GlobalKey<FormState>. Each input field should be a TextFormField (or similar) with validator and optionally onSaved functions.

DARTRead-only
1
final _formKey = GlobalKey<FormState>();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(
        decoration: InputDecoration(labelText: 'Email'),
        validator: (value) {
          if (value == null || value.isEmpty) {
            return 'Please enter your email';
          }
          return null;
        },
      ),
      TextFormField(
        decoration: InputDecoration(labelText: 'Password'),
        obscureText: true,
        validator: (value) {
          if (value == null || value.length < 6) {
            return 'Password must be at least 6 characters';
          }
          return null;
        },
      ),
      ElevatedButton(
        onPressed: () {
          if (_formKey.currentState!.validate()) {
            _formKey.currentState!.save();
            // process data
          }
        },
        child: Text('Submit'),
      ),
    ],
  ),
)

Key Components

  • Form – The container widget that holds the form fields and manages their state.
  • GlobalKey<FormState> – A unique identifier that allows you to access the form's state (validate, save, reset).
  • TextFormField – A Material Design text input field that integrates with Form. It provides validator and onSaved callbacks.
  • validator – A function that returns an error string if the input is invalid, or null if valid.
  • onSaved – A function that is called when _formKey.currentState!.save() is invoked, typically to store the field value.
  • autovalidateMode – Determines when validation runs (e.g., AutovalidateMode.onUserInteraction).

Validating Form Fields

The validator function is called when _formKey.currentState!.validate() is executed. It should return an error string if the input is invalid, or null if it's valid. The error string is displayed below the field.

DARTRead-only
1
TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'This field is required';
    }
    // Email regex example
    if (!RegExp(r'^[^@]+@[^@]+\.[^@]+').hasMatch(value)) {
      return 'Enter a valid email';
    }
    return null;
  },
)

Saving Form Data

Use the onSaved callback to store the field value when _formKey.currentState!.save() is called. You typically do this after validation passes.

DARTRead-only
1
String _email = '';
String _password = '';

TextFormField(
  onSaved: (value) => _email = value!,
)
TextFormField(
  onSaved: (value) => _password = value!,
)

// After validation
_formKey.currentState!.save();
// Now _email and _password contain the entered values

Resetting the Form

To clear all fields and reset validation errors, call _formKey.currentState!.reset().

Using Controllers for Fine Control

If you need to programmatically change the text or listen to changes, use a TextEditingController for each field. Remember to dispose them in State.dispose().

DARTRead-only
1
final _emailController = TextEditingController();

@override
void dispose() {
  _emailController.dispose();
  super.dispose();
}

TextFormField(
  controller: _emailController,
  validator: ...
)

Common Mistakes Beginners Make

  • Not using a GlobalKey: Without a key, you cannot access the form state to validate or save.
  • Forgetting to wrap fields with Form: TextFormField only works properly inside a Form widget.
  • Not handling null in validator: Always check for null because the value can be null, especially if the field is empty.
  • Calling save() before validate(): Always validate first; otherwise you might save invalid data.
  • Using onChanged for validation instead of validator: The validator is integrated with the form's validation system and automatically shows errors.
  • Not disposing controllers: If you use TextEditingController, dispose them to avoid memory leaks.
  • Setting autovalidateMode to always: Can be annoying for users; use onUserInteraction for a better experience.

Key Points to Remember

  • Wrap your input fields with a Form and provide a GlobalKey<FormState>.
  • Use TextFormField for text input; it has built‑in form integration.
  • Define validator to check input and return an error string when invalid.
  • Call _formKey.currentState!.validate() to trigger validation.
  • After successful validation, call _formKey.currentState!.save() to populate your variables via onSaved.
  • Use TextEditingController if you need to control or listen to the text.
  • Always dispose controllers in State.dispose().

Common Interview Questions

  1. How does Form widget manage its children's state?
  2. What is the role of GlobalKey<FormState> in a form?
  3. Explain the difference between validator and onSaved.
  4. How would you implement password confirmation validation?
  5. What is autovalidateMode and when would you use it?
  6. How do you programmatically set the focus to a specific form field?
  7. How can you handle asynchronous validation (e.g., checking if a username is already taken)?
  8. What happens if you don't provide a validator for a field? Can it still be validated?

Try it yourself

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Form Example')),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: MyForm(),
        ),
      ),
    );
  }
}

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

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

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      // Show success message
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Form submitted successfully!')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          TextFormField(
            controller: _emailController,
            decoration: InputDecoration(
              labelText: 'Email',
              border: OutlineInputBorder(),
            ),
            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;
            },
            onSaved: (value) {
              // You could save to a variable here
              print('Email saved: $value');
            },
          ),
          SizedBox(height: 16),
          TextFormField(
            controller: _passwordController,
            obscureText: true,
            decoration: InputDecoration(
              labelText: 'Password',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.isEmpty) {
                return 'Please enter your password';
              }
              if (value.length < 6) {
                return 'Password must be at least 6 characters';
              }
              return null;
            },
            onSaved: (value) {
              print('Password saved: $value');
            },
          ),
          SizedBox(height: 24),
          Center(
            child: ElevatedButton(
              onPressed: _submitForm,
              child: Text('Submit'),
            ),
          ),
        ],
      ),
    );
  }
}

Test Your Knowledge

Q1
of 3

What is the purpose of a `GlobalKey<FormState>` in a Flutter form?

A
To uniquely identify the form for theming
B
To access the form's state (validate, save, reset)
C
To store form field values
D
To provide a key for the form's animation
Q2
of 3

Which callback is used to validate a TextFormField?

A
onChanged
B
onSaved
C
validator
D
onFieldSubmitted
Q3
of 3

When should you call `_formKey.currentState!.save()`?

A
Before validation
B
After validation succeeds
C
On every keystroke
D
Only when resetting the form

Previous

flutter textfield

Next

flutter card

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.