Popular Searches
Popular Course Categories
Popular Courses

Flutter Forms

Flutter Forms & User Input

Flutter Forms – Detailed Notes

Forms are an important part of Flutter application development. A form allows an application to collect, organize, validate, save, and reset information entered by users. Common examples include login forms, registration forms, contact forms, profile forms, checkout forms, search forms, and feedback forms.

Flutter provides the Form widget for grouping form fields and managing operations such as validation, saving, and resetting. TextFormField is commonly used inside a Form because it integrates text input with Flutter's form system. Flutter Forms Cookbook


1. What is a Form in Flutter?

A Form is a widget that groups multiple form fields into one logical unit. It helps developers manage the state and validation of fields together.

A typical form may contain:

  • Name field
  • Email field
  • Phone number field
  • Password field
  • Address field
  • Dropdown field
  • Checkbox
  • Radio buttons
  • Submit button
  • Reset button

Basic Structure

Form(
  child: Column(
    children: [
      TextFormField(),
      TextFormField(),
      ElevatedButton(
        onPressed: () {},
        child: const Text('Submit'),
      ),
    ],
  ),
)

2. Why Use Forms?

Forms make it easier to manage multiple user-input fields as a single unit.

Major Benefits

  • Group multiple fields together.
  • Validate multiple fields.
  • Display validation errors.
  • Save form values.
  • Reset fields.
  • Manage form state.
  • Provide a consistent user experience.
  • Make data submission easier.

3. Form and FormField Relationship

Flutter's Form widget acts as a common ancestor for form fields. Individual fields can be implemented using widgets derived from FormField, including TextFormField.

Form
 |
 +-- TextFormField
 |
 +-- TextFormField
 |
 +-- DropdownButtonFormField
 |
 +-- Checkbox
 |
 +-- Submit Button

The FormState provides methods that can validate, save, and reset the descendant form fields. Flutter Form API


4. Creating a Basic Form

The following example demonstrates a simple form containing a name field and a submit button.

import 'package:flutter/material.dart';

class BasicForm extends StatelessWidget {
  const BasicForm({super.key});

  @override
  Widget build(BuildContext context) {
    return Form(
      child: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(
              labelText: 'Name',
              border: OutlineInputBorder(),
            ),
          ),
          const SizedBox(height: 16),
          ElevatedButton(
            onPressed: () {},
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}

5. Form with GlobalKey

A GlobalKey is commonly used to access the state of a form from the surrounding stateful widget. It allows you to call methods such as validate(), save(), and reset().

final _formKey = GlobalKey();

Form(
  key: _formKey,
  child: Column(
    children: [
      TextFormField(),
    ],
  ),
)

Flutter's documentation recommends using a GlobalKey as a convenient way to access the form's state when validation or other form operations need to be triggered from outside the form itself. Flutter Form Validation


6. Understanding FormState

FormState represents the mutable state associated with a Form. It provides methods for managing the fields belonging to that form.

Important FormState Methods

Method Purpose
validate() Validates all form fields.
save() Calls the onSaved callback of each form field.
reset() Resets form fields to their initial values.
clearError() Clears validation errors without resetting field values.

Flutter FormState API


7. TextFormField

TextFormField is a form-aware text input widget. It wraps a TextField with FormField functionality, making it convenient for validation and other form operations.

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Username',
    border: OutlineInputBorder(),
  ),
)

Flutter's API documentation describes TextFormField as a convenience widget that wraps a TextField in a FormField. TextFormField API


8. TextField vs TextFormField

Feature TextField TextFormField
Accepts text input Yes Yes
Works inside Form Can be used Designed for it
Built-in validator property No Yes
onSaved callback No Yes
FormState integration No Yes
Useful for structured forms Less suitable Yes

For a form that needs validation, TextFormField is generally the more appropriate widget. Flutter Text Input Documentation


9. Form Validation

Validation checks whether the information entered by the user meets the application's requirements.

Examples:

  • Name should not be empty.
  • Email should contain a valid structure.
  • Password should meet minimum requirements.
  • Phone number should contain an appropriate number of digits.
  • Age should be within an acceptable range.

Basic Validator

TextFormField(
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please enter your name';
    }
    return null;
  },
)

A validator returns an error message when input is invalid and null when the value is valid. Flutter Validation Recipe


10. Validating the Complete Form

Use the form key to validate all fields.

if (_formKey.currentState!.validate()) {
  print('Form is valid');
} else {
  print('Form contains errors');
}

When validate() is called, Flutter runs the validators of the form fields and returns true when there are no validation errors. FormState.validate()


11. Complete Form Validation Example

import 'package:flutter/material.dart';

class RegistrationForm extends StatefulWidget {
  const RegistrationForm({super.key});

  @override
  State createState() => _RegistrationFormState();
}

class _RegistrationFormState extends State {
  final _formKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(
            decoration: const InputDecoration(
              labelText: 'Name',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return 'Please enter your name';
              }
              return null;
            },
          ),
          const SizedBox(height: 16),
          TextFormField(
            decoration: const InputDecoration(
              labelText: 'Email',
              border: OutlineInputBorder(),
            ),
            validator: (value) {
              if (value == null || value.trim().isEmpty) {
                return 'Please enter your email';
              }
              return null;
            },
          ),
          const SizedBox(height: 20),
          ElevatedButton(
            onPressed: () {
              if (_formKey.currentState!.validate()) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text('Form submitted successfully'),
                  ),
                );
              }
            },
            child: const Text('Submit'),
          ),
        ],
      ),
    );
  }
}

12. Using TextEditingController with Forms

A TextEditingController can be used when the application needs direct access to the text entered by the user.

final nameController = TextEditingController();

TextFormField(
  controller: nameController,
  decoration: const InputDecoration(
    labelText: 'Name',
  ),
)

Read the value using:

final name = nameController.text.trim();

Dispose the Controller

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

Flutter recommends disposing a TextEditingController when it is no longer needed. Retrieve Text Field Values


13. Using Multiple Controllers

For a registration form, separate controllers can be created for different fields.

final nameController = TextEditingController();
final emailController = TextEditingController();
final phoneController = TextEditingController();
final passwordController = TextEditingController();

Dispose all controllers:

@override
void dispose() {
  nameController.dispose();
  emailController.dispose();
  phoneController.dispose();
  passwordController.dispose();
  super.dispose();
}

14. Saving Form Data with onSaved

The onSaved callback can be used to store a field's value when FormState.save() is called.

String name = '';

TextFormField(
  decoration: const InputDecoration(
    labelText: 'Name',
  ),
  onSaved: (value) {
    name = value?.trim() ?? '';
  },
)

Calling save()

if (_formKey.currentState!.validate()) {
  _formKey.currentState!.save();
  print(name);
}

FormState.save() calls the onSaved callback for each descendant form field. FormState.save()


15. Resetting a Form

The reset() method restores form fields to their initial values and clears validation state.

_formKey.currentState!.reset();

Reset Button

OutlinedButton(
  onPressed: () {
    _formKey.currentState!.reset();
  },
  child: const Text('Reset'),
)

Reset functionality is useful when users want to clear or restore a form.


16. Form with Name, Email, Phone, and Password

import 'package:flutter/material.dart';

class RegistrationScreen extends StatefulWidget {
  const RegistrationScreen({super.key});

  @override
  State createState() => _RegistrationScreenState();
}

class _RegistrationScreenState extends State {
  final _formKey = GlobalKey();

  final nameController = TextEditingController();
  final emailController = TextEditingController();
  final phoneController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    phoneController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  void submitForm() {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    final name = nameController.text.trim();
    final email = emailController.text.trim();
    final phone = phoneController.text.trim();
    final password = passwordController.text;

    print('Name: $name');
    print('Email: $email');
    print('Phone: $phone');
    print('Password received');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Registration Form'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: nameController,
                decoration: const InputDecoration(
                  labelText: 'Full Name',
                  prefixIcon: Icon(Icons.person),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Name is required';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email Address',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Email is required';
                  }
                  if (!value.contains('@')) {
                    return 'Enter a valid email address';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: phoneController,
                keyboardType: TextInputType.phone,
                decoration: const InputDecoration(
                  labelText: 'Phone Number',
                  prefixIcon: Icon(Icons.phone),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Phone number is required';
                  }
                  if (value.trim().length < 10) {
                    return 'Enter a valid phone number';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: passwordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Password',
                  prefixIcon: Icon(Icons.lock),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Password is required';
                  }
                  if (value.length < 6) {
                    return 'Password must contain at least 6 characters';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: submitForm,
                  child: const Text('Register'),
                ),
              ),
              const SizedBox(height: 10),
              SizedBox(
                width: double.infinity,
                child: OutlinedButton(
                  onPressed: () {
                    _formKey.currentState!.reset();
                    nameController.clear();
                    emailController.clear();
                    phoneController.clear();
                    passwordController.clear();
                  },
                  child: const Text('Reset'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

17. Email Validation

A basic email validation can check whether the input contains an expected email structure.

TextFormField(
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email',
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Email is required';
    }

    final email = value.trim();

    if (!email.contains('@')) {
      return 'Enter a valid email';
    }

    return null;
  },
)

For production applications, use validation rules appropriate to the application's actual requirements rather than assuming that a simple string check guarantees a valid deliverable email address.


18. Password Validation

TextFormField(
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Password',
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Password is required';
    }

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

    return null;
  },
)

Important Password Practices

  • Do not display passwords as plain text unless the user explicitly chooses to reveal them.
  • Do not print passwords to the console.
  • Do not store passwords insecurely.
  • Use secure authentication services for real applications.
  • Apply password requirements based on the application's security policy.

19. Confirm Password Validation

A registration form may require users to enter the password twice.

final passwordController = TextEditingController();

final confirmPasswordController = TextEditingController();

TextFormField(
  controller: confirmPasswordController,
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Confirm Password',
  ),
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'Please confirm your password';
    }

    if (value != passwordController.text) {
      return 'Passwords do not match';
    }

    return null;
  },
)

20. Phone Number Validation

TextFormField(
  keyboardType: TextInputType.phone,
  decoration: const InputDecoration(
    labelText: 'Phone Number',
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Phone number is required';
    }

    final phone = value.trim();

    if (phone.length < 10) {
      return 'Enter a valid phone number';
    }

    return null;
  },
)

For international applications, phone-number validation should account for the application's supported countries and formats.


21. Using AutovalidateMode

Flutter provides AutovalidateMode to control when validation messages are displayed.

Validate After User Interaction

TextFormField(
  autovalidateMode: AutovalidateMode.onUserInteraction,
  validator: (value) {
    if (value == null || value.isEmpty) {
      return 'This field is required';
    }
    return null;
  },
)

Common Modes

Mode Meaning
AutovalidateMode.disabled Automatic validation is disabled.
AutovalidateMode.always Validation occurs continuously.
AutovalidateMode.onUserInteraction Validation occurs after user interaction.

22. Form Field Error Messages

A validator returns a string when a field is invalid.

validator: (value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your name';
  }
  return null;
}

Flutter displays the returned error message as part of the form field's validation UI.


23. Styling Form Fields

InputDecoration can be used to create a professional form interface.

TextFormField(
  decoration: InputDecoration(
    labelText: 'Full Name',
    hintText: 'Enter your full name',
    prefixIcon: const Icon(Icons.person),
    filled: true,
    border: OutlineInputBorder(
      borderRadius: BorderRadius.circular(12),
    ),
  ),
)

Flutter's text-field documentation demonstrates using InputDecoration for labels, icons, hints, borders, and other visual elements. Flutter TextField Styling


24. Form with Dropdown

Forms can contain controls other than text fields. For example, a dropdown can collect a user's selected role.

String? selectedRole;

DropdownButtonFormField(
  value: selectedRole,
  decoration: const InputDecoration(
    labelText: 'Role',
    border: OutlineInputBorder(),
  ),
  items: const [
    DropdownMenuItem(
      value: 'Student',
      child: Text('Student'),
    ),
    DropdownMenuItem(
      value: 'Developer',
      child: Text('Developer'),
    ),
    DropdownMenuItem(
      value: 'Designer',
      child: Text('Designer'),
    ),
  ],
  onChanged: (value) {
    selectedRole = value;
  },
)

25. Form with Checkbox

Checkboxes are useful when a form requires a true or false choice.

bool acceptedTerms = false;

CheckboxListTile(
  title: const Text('I agree to the terms'),
  value: acceptedTerms,
  onChanged: (value) {
    setState(() {
      acceptedTerms = value ?? false;
    });
  },
)

Validating Terms Acceptance

if (!acceptedTerms) {
  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('Please accept the terms'),
    ),
  );
  return;
}

26. Form with Radio Buttons

Radio buttons can be used when the user must select one option from a group.

String? selectedGender;

Column(
  children: [
    RadioListTile(
      title: const Text('Male'),
      value: 'Male',
      groupValue: selectedGender,
      onChanged: (value) {
        setState(() {
          selectedGender = value;
        });
      },
    ),
    RadioListTile(
      title: const Text('Female'),
      value: 'Female',
      groupValue: selectedGender,
      onChanged: (value) {
        setState(() {
          selectedGender = value;
        });
      },
    ),
  ],
)

27. Form with Date Picker

A form may also collect dates such as date of birth or appointment dates.

DateTime? selectedDate;

Future selectDate() async {
  final date = await showDatePicker(
    context: context,
    firstDate: DateTime(1900),
    lastDate: DateTime.now(),
    initialDate: DateTime.now(),
  );

  if (date != null) {
    setState(() {
      selectedDate = date;
    });
  }
}

Button Example

OutlinedButton(
  onPressed: selectDate,
  child: Text(
    selectedDate == null
        ? 'Select Date'
        : '${selectedDate!.day}/${selectedDate!.month}/${selectedDate!.year}',
  ),
)

28. Form with Multiline Text

Multiline fields are useful for addresses, descriptions, feedback, and comments.

TextFormField(
  maxLines: 5,
  decoration: const InputDecoration(
    labelText: 'Address',
    alignLabelWithHint: true,
    border: OutlineInputBorder(),
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Address is required';
    }
    return null;
  },
)

29. Managing Focus in Forms

Focus management improves the experience when a form contains multiple fields.

final nameFocusNode = FocusNode();
final emailFocusNode = FocusNode();

TextFormField(
  focusNode: nameFocusNode,
  textInputAction: TextInputAction.next,
  onFieldSubmitted: (_) {
    emailFocusNode.requestFocus();
  },
)

TextFormField(
  focusNode: emailFocusNode,
  textInputAction: TextInputAction.done,
)

Focus nodes are long-lived objects and should be managed through the state lifecycle when owned by a stateful widget. Flutter Focus and Text Fields


30. Complete Login Form

import 'package:flutter/material.dart';

class LoginForm extends StatefulWidget {
  const LoginForm({super.key});

  @override
  State createState() => _LoginFormState();
}

class _LoginFormState extends State {
  final _formKey = GlobalKey();
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  void login() {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    final email = emailController.text.trim();
    final password = passwordController.text;

    print('Login requested for: $email');
    print('Password received');
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Login'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  prefixIcon: Icon(Icons.email),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Email is required';
                  }
                  if (!value.contains('@')) {
                    return 'Enter a valid email';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: passwordController,
                obscureText: true,
                decoration: const InputDecoration(
                  labelText: 'Password',
                  prefixIcon: Icon(Icons.lock),
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.isEmpty) {
                    return 'Password is required';
                  }
                  if (value.length < 6) {
                    return 'Password must contain at least 6 characters';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 24),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: login,
                  child: const Text('Login'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

31. Complete Contact Form

import 'package:flutter/material.dart';

class ContactForm extends StatefulWidget {
  const ContactForm({super.key});

  @override
  State createState() => _ContactFormState();
}

class _ContactFormState extends State {
  final _formKey = GlobalKey();
  final nameController = TextEditingController();
  final emailController = TextEditingController();
  final messageController = TextEditingController();

  @override
  void dispose() {
    nameController.dispose();
    emailController.dispose();
    messageController.dispose();
    super.dispose();
  }

  void sendMessage() {
    if (!_formKey.currentState!.validate()) {
      return;
    }

    final name = nameController.text.trim();
    final email = emailController.text.trim();
    final message = messageController.text.trim();

    print('Name: $name');
    print('Email: $email');
    print('Message: $message');

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Message submitted successfully'),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Contact Us'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Form(
          key: _formKey,
          child: Column(
            children: [
              TextFormField(
                controller: nameController,
                decoration: const InputDecoration(
                  labelText: 'Name',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Please enter your name';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: emailController,
                keyboardType: TextInputType.emailAddress,
                decoration: const InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Please enter your email';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 16),
              TextFormField(
                controller: messageController,
                maxLines: 5,
                decoration: const InputDecoration(
                  labelText: 'Message',
                  alignLabelWithHint: true,
                  border: OutlineInputBorder(),
                ),
                validator: (value) {
                  if (value == null || value.trim().isEmpty) {
                    return 'Please enter your message';
                  }
                  return null;
                },
              ),
              const SizedBox(height: 20),
              SizedBox(
                width: double.infinity,
                child: ElevatedButton(
                  onPressed: sendMessage,
                  child: const Text('Send Message'),
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

32. Saving Form Data into an Object

After validation, form values can be collected into a model object.

class User {
  final String name;
  final String email;
  final String phone;

  User({
    required this.name,
    required this.email,
    required this.phone,
  });
}

Creating the Object

final user = User(
  name: nameController.text.trim(),
  email: emailController.text.trim(),
  phone: phoneController.text.trim(),
);

This approach helps separate the user interface from the application's data model.


33. Form Submission Flow

A typical Flutter form follows this sequence:

  1. User opens the form.
  2. User enters information.
  3. Application captures the input.
  4. User presses the submit button.
  5. Form validation runs.
  6. Invalid fields display error messages.
  7. If valid, the application collects the values.
  8. Data is processed, saved, or sent to an API.
  9. User receives a success or failure message.
  10. Form may be reset if appropriate.
User Input
    |
    v
TextFormField
    |
    v
Form
    |
    v
Validate
    |
    +---- Invalid ----> Show Error
    |
    +---- Valid ------> Save/Process Data
                              |
                              v
                       API / Database

34. Form with Loading State

When a form performs an asynchronous operation such as an API request, a loading state can prevent duplicate submissions.

bool isLoading = false;

Future submitForm() async {
  if (!_formKey.currentState!.validate()) {
    return;
  }

  setState(() {
    isLoading = true;
  });

  try {
    await Future.delayed(const Duration(seconds: 2));

    if (!mounted) return;

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Form submitted successfully'),
      ),
    );
  } finally {
    if (mounted) {
      setState(() {
        isLoading = false;
      });
    }
  }
}

Loading Button

ElevatedButton(
  onPressed: isLoading ? null : submitForm,
  child: isLoading
      ? const CircularProgressIndicator()
      : const Text('Submit'),
)

35. Form Inside SingleChildScrollView

Long forms may not fit on smaller screens. Wrapping the form in SingleChildScrollView allows users to scroll through the fields.

Scaffold(
  body: SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: Form(
      key: _formKey,
      child: Column(
        children: [
          TextFormField(),
          TextFormField(),
          TextFormField(),
          ElevatedButton(
            onPressed: submitForm,
            child: const Text('Submit'),
          ),
        ],
      ),
    ),
  ),
)

36. Form Keyboard Handling

When a keyboard appears, long forms can become difficult to use. A scrollable layout helps prevent important fields and buttons from being hidden.

Useful Practices

  • Use SingleChildScrollView for long forms.
  • Use appropriate TextInputAction values.
  • Move focus between fields.
  • Use FocusNode where necessary.
  • Keep form spacing consistent.
  • Test forms on different screen sizes.

37. Common Form Properties

Property Widget Purpose
key Form Identifies and provides access to the form state.
autovalidateMode Form/FormField Controls automatic validation.
onChanged Form Runs when a form field changes.
validator FormField/TextFormField Validates a field.
onSaved FormField/TextFormField Saves a field value when FormState.save() is called.
initialValue FormField/TextFormField Provides an initial field value when a controller is not used.
controller TextFormField Provides programmatic access to text.

38. Common Form Methods

Method Example Use
validate _formKey.currentState!.validate() Validate all fields.
save _formKey.currentState!.save() Save all field values through onSaved.
reset _formKey.currentState!.reset() Reset form fields.
clearError _formKey.currentState!.clearError() Clear form validation errors.

39. Common Mistakes in Flutter Forms

Mistake 1: Creating a New GlobalKey in build()

@override
Widget build(BuildContext context) {
  final formKey = GlobalKey();
  return Form(
    key: formKey,
    child: Container(),
  );
}

For a stateful form, keep the key as a state field so it remains stable across rebuilds.

Better Approach

class FormScreenState extends State {
  final formKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Form(
      key: formKey,
      child: Container(),
    );
  }
}

Mistake 2: Forgetting Validators

A form does not automatically know whether an email, password, or name is valid. Add validation logic appropriate to the application.

Mistake 3: Forgetting to Dispose Controllers

@override
void dispose() {
  nameController.dispose();
  emailController.dispose();
  super.dispose();
}

Mistake 4: Making a Long Form Non-Scrollable

Use a suitable scrollable layout for forms that may exceed the available screen height.

Mistake 5: Logging Sensitive Data

Never unnecessarily print passwords or other sensitive information to the console.


40. Best Practices for Flutter Forms

  • Use Form to group related fields.
  • Use TextFormField for text fields that participate in form validation.
  • Keep the GlobalKey stable.
  • Use meaningful validation messages.
  • Validate before submitting data.
  • Use TextEditingController when programmatic access is needed.
  • Dispose controllers when they are no longer needed.
  • Use appropriate keyboard types.
  • Use obscureText for passwords.
  • Use SingleChildScrollView for long forms where appropriate.
  • Manage keyboard focus between fields.
  • Prevent duplicate submissions during asynchronous operations.
  • Never log sensitive credentials.
  • Keep validation rules consistent with server-side validation.
  • Test forms with valid, invalid, empty, and unexpected input.

41. Practical Form Development Example

Consider a student registration application. The form may contain:

  • Student name
  • Email address
  • Phone number
  • Course selection
  • Password
  • Confirm password
  • Terms and conditions checkbox
  • Register button

Processing Flow

Student opens registration screen
        |
        v
Enters personal information
        |
        v
Selects course
        |
        v
Enters password
        |
        v
Accepts terms
        |
        v
Clicks Register
        |
        v
Validate all fields
        |
        +---- Invalid ----> Show errors
        |
        +---- Valid ------> Create user data
                              |
                              v
                         Send to API
                              |
                              v
                       Show result

42. Form Validation Checklist

  • Is the required field empty?
  • Is the email format acceptable?
  • Is the phone number valid for the application's supported format?
  • Does the password meet the application's requirements?
  • Do password and confirmation match?
  • Has the required dropdown option been selected?
  • Has the user accepted required terms?
  • Are numeric values within the expected range?
  • Are text lengths within the required limits?
  • Is the submitted data safe to process?

43. Flutter Forms vs Manual Input Handling

Feature Manual Input Handling Flutter Form
Multiple fields Requires custom handling Grouped together
Validation Manual logic Validator support
Reset Manual clearing FormState.reset()
Save Manual processing FormState.save()
Validation status Custom state FormState.validate()
Suitable for complex forms Can become repetitive Designed for structured forms

44. Interview Questions

Q1. What is the Form widget in Flutter?

Answer: Form is a widget used to group and manage multiple form fields. It provides access to form operations such as validation, saving, and resetting.

Q2. What is TextFormField?

Answer: TextFormField is a form-aware text input widget that wraps a TextField in a FormField and provides features such as validation and saving.

Q3. Why is GlobalKey used?

Answer: It provides access to the associated FormState, allowing code to call methods such as validate(), save(), and reset().

Q4. What does validate() return?

Answer: It returns true when all descendant form fields pass validation and false when validation errors exist.

Q5. What does the validator return?

Answer: It returns an error message string when the input is invalid and null when the input is valid.

Q6. What is the purpose of onSaved?

Answer: It provides a callback that can receive a field's value when FormState.save() is called.

Q7. How do you reset a Flutter form?

_formKey.currentState!.reset();

Q8. Can TextEditingController be used with TextFormField?

Answer: Yes. A TextEditingController can be passed to the controller property of a TextFormField.


45. Quick Revision

  • Form groups related form fields.
  • TextFormField is useful for text input inside forms.
  • GlobalKey provides convenient access to form state.
  • validate() checks all form validators.
  • save() triggers field onSaved callbacks.
  • reset() restores fields to their initial values.
  • validator returns an error message or null.
  • onSaved can be used to collect field values.
  • TextEditingController provides programmatic access to text.
  • AutovalidateMode controls automatic validation behavior.
  • FocusNode helps manage keyboard focus.
  • Long forms should be designed for scrolling and different screen sizes.
  • Always validate important data before processing or submitting it.

46. Official Flutter Resources


47. JustAcademy Flutter Training Resources

For additional Flutter training and course information, visit the following resources:


48. Key Takeaways

Flutter Forms provide a structured way to collect and manage user information. The Form widget groups related fields, while TextFormField provides form-aware text input. A GlobalKey can be used to access the form state and perform operations such as validation, saving, and resetting.

For practical Flutter applications, forms are commonly combined with TextEditingController, validators, focus management, input formatting, loading states, and API calls. Learning these concepts provides a strong foundation for building login screens, registration screens, contact forms, profile forms, checkout forms, and other real-world Flutter applications.

Previous Next
whatsapp