Popular Searches
Popular Course Categories
Popular Courses

Creating structured forms

Creating structured forms

Flutter Forms & User Input


Creating Structured Forms in Flutter


Structured forms are an important part of Flutter application development. A structured form organizes multiple input fields into a single logical unit so that user data can be collected, validated, saved, reset, and submitted efficiently. Flutter provides the Form, FormField, and TextFormField widgets for building structured forms. The Form widget acts as a common container for multiple form fields and provides access to operations such as validation, saving, and resetting. :contentReference[oaicite:0]{index=0}




1. What is a Structured Form?


A structured form is a collection of related input controls arranged in a meaningful order. Instead of treating every input field independently, a form groups related fields together and manages their behavior as one unit.


Example


A student registration form can contain:



  • Student Name

  • Email Address

  • Phone Number

  • Date of Birth

  • Course

  • Password

  • Confirm Password

  • Terms and Conditions

  • Submit Button

  • Reset Button


All these fields can be placed inside one Flutter Form.




2. Why Use Structured Forms?


Structured forms make applications easier to develop, maintain, validate, and use.



  • Group related fields together.

  • Validate multiple fields at once.

  • Display meaningful validation messages.

  • Save field values systematically.

  • Reset fields when required.

  • Manage keyboard focus.

  • Control user input.

  • Submit data to APIs or databases.

  • Improve the user experience.

  • Make large forms easier to maintain.




3. Main Flutter Form Widgets











Widget/ClassPurpose
FormGroups multiple form fields and manages form-level state.
FormFieldRepresents an individual form field with its own state and validation.
TextFormFieldProvides text input integrated with the Form system.
TextEditingControllerReads and controls text entered by the user.
GlobalKeyProvides access to the FormState for validation, saving, and resetting.
FocusNodeControls keyboard focus between form fields.
InputDecorationControls labels, hints, borders, icons, and other field decoration.

Flutter's API describes Form as an optional container for grouping multiple form fields and using FormState to save, reset, or validate those fields. :contentReference[oaicite:1]{index=1}




4. Basic Structure of a Structured Form


Form(
  key: formKey,
  child: Column(
    children: [
      TextFormField(),
      TextFormField(),
      ElevatedButton(
        onPressed: submitForm,
        child: const Text('Submit'),
      ),
    ],
  ),
)

A typical structured form contains:



  1. A form key.

  2. Input fields.

  3. Validation rules.

  4. Data collection logic.

  5. Submit functionality.

  6. Reset functionality.

  7. Optional focus management.




5. Creating GlobalKey


A GlobalKey is commonly used when the application needs to access the state of a form. It allows code to call methods such as validate(), save(), and reset(). Flutter's documentation recommends using a GlobalKey as a straightforward way to access form state. :contentReference[oaicite:2]{index=2}


final GlobalKey formKey =
    GlobalKey();

The key is then assigned to the Form:


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



6. Why Should the Form Key Be Stored?


The form key should normally be created once and stored as part of the state rather than being recreated every time build() runs.


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

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

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

  @override
  Widget build(BuildContext context) {
    return Form(
      key: formKey,
      child: const Column(
        children: [],
      ),
    );
  }
}




7. TextFormField in Structured Forms


TextFormField is especially useful in structured forms because it integrates a text field with Flutter's FormField system and supports validation and saving. :contentReference[oaicite:3]{index=3}


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



8. Creating a Basic Structured Form


import 'package:flutter/material.dart';

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

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('User Form'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Form(
          key: formKey,
          child: Column(
            children: [
              TextFormField(
                decoration: const InputDecoration(
                  labelText: 'Name',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 16),
              TextFormField(
                decoration: const InputDecoration(
                  labelText: 'Email',
                  border: OutlineInputBorder(),
                ),
              ),
              const SizedBox(height: 16),
              ElevatedButton(
                onPressed: () {},
                child: const Text('Submit'),
              ),
            ],
          ),
        ),
      ),
    );
  }
}




9. Organizing Fields into Sections


Large forms should be divided into logical sections instead of displaying a long list of unrelated fields.


Example Sections



  • Personal Information

  • Contact Information

  • Course Information

  • Account Information

  • Terms and Conditions


Example


Form(
  key: formKey,
  child: Column(
    crossAxisAlignment: CrossAxisAlignment.start,
    children: [
      const Text(
        'Personal Information',
        style: TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
        ),
      ),
      const SizedBox(height: 16),
      TextFormField(),
      const SizedBox(height: 16),
      TextFormField(),
      const SizedBox(height: 30),
      const Text(
        'Contact Information',
        style: TextStyle(
          fontSize: 20,
          fontWeight: FontWeight.bold,
        ),
      ),
      const SizedBox(height: 16),
      TextFormField(),
    ],
  ),
)



10. Using Form Validation


Validation ensures that users provide acceptable information before the application processes the form. Flutter's validator callback returns an error message when a field is invalid and returns null when the value is valid. :contentReference[oaicite:4]{index=4}


Required Field


TextFormField(
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Name is required';
    }
    return null;
  },
)



11. Validating the Entire Form


When the user presses Submit, call validate() on the form state.


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

When validate() is called, Flutter runs the validators associated with the form fields. It returns true when there are no validation errors and false when validation errors exist. :contentReference[oaicite:5]{index=5}




12. Required Field Validation


TextFormField(
  decoration: const InputDecoration(
    labelText: 'Full Name',
  ),
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return 'Please enter your full name';
    }
    return null;
  },
)



13. Email Field


TextFormField(
  keyboardType: TextInputType.emailAddress,
  decoration: const InputDecoration(
    labelText: 'Email Address',
    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;
  },
)




14. Phone Number Field


TextFormField(
  keyboardType: TextInputType.phone,
  decoration: const InputDecoration(
    labelText: 'Phone Number',
    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;
  },
)




15. Password Field


TextFormField(
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Password',
    border: OutlineInputBorder(),
  ),
  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;
  },
)




16. Confirm Password Field


A structured registration form often contains a password confirmation field. The second password can be compared with the first password.


final passwordController = TextEditingController();

TextFormField(
  controller: passwordController,
  obscureText: true,
  decoration: const InputDecoration(
    labelText: 'Password',
  ),
)

TextFormField(
  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;
  },
)




17. Using TextEditingController


A TextEditingController is useful when the application needs direct access to field values, needs to prefill fields, or needs to modify and clear input programmatically. Flutter's documentation also recommends disposing the controller when it is no longer needed. :contentReference[oaicite:6]{index=6}


final nameController = TextEditingController();

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


Reading the Value


final name = nameController.text.trim();

Clearing the Value


nameController.clear();



18. Managing Multiple Controllers


A larger form may require multiple controllers.


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

Dispose Controllers


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



19. Collecting Form Data


After successful validation, controller values can be collected and converted into variables or a model.


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

  final name = nameController.text.trim();
  final email = emailController.text.trim();
  final phone = phoneController.text.trim();
  final address = addressController.text.trim();

  print('Name: $name');
  print('Email: $email');
  print('Phone: $phone');
  print('Address: $address');
}




20. Using onSaved


Instead of using controllers for every field, a FormField can use its onSaved callback to store the value when FormState.save() is called.


String name = '';
String email = '';

TextFormField(
  onSaved: (value) {
    name = value?.trim() ?? '';
  },
)

TextFormField(
  onSaved: (value) {
    email = value?.trim() ?? '';
  },
)


Calling save()


if (formKey.currentState!.validate()) {
  formKey.currentState!.save();

  print(name);
  print(email);
}


FormState.save() calls the onSaved callback of each descendant form field. :contentReference[oaicite:7]{index=7}




21. Resetting a Structured Form


Flutter provides FormState.reset() to reset the form fields and their validation state.


formKey.currentState!.reset();

Reset Button


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

If controllers are being used, clear them as well when the intended behavior is to empty the text fields.


void resetForm() {
  formKey.currentState!.reset();
  nameController.clear();
  emailController.clear();
  phoneController.clear();
}



22. Automatic Validation


autovalidateMode controls when form fields automatically display validation errors.







ModeMeaning
AutovalidateMode.disabledAutomatic validation is disabled.
AutovalidateMode.alwaysValidation runs automatically.
AutovalidateMode.onUserInteractionValidation occurs as the user interacts with the field.

Example


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



23. Handling Form Changes


The Form widget provides an onChanged callback that is called when a form field changes. :contentReference[oaicite:8]{index=8}


Form(
  key: formKey,
  onChanged: () {
    print('Form changed');
  },
  child: Column(
    children: [
      TextFormField(),
      TextFormField(),
    ],
  ),
)



24. Managing Keyboard Focus


Good focus management makes forms easier to use. Users can move from one field to another using the keyboard's Next action. Flutter provides FocusNode and FocusScope for managing focus. :contentReference[oaicite:9]{index=9}


Create FocusNodes


final nameFocus = FocusNode();
final emailFocus = FocusNode();
final phoneFocus = FocusNode();

Connect FocusNode


TextFormField(
  focusNode: nameFocus,
  textInputAction: TextInputAction.next,
)

Move to Next Field


TextFormField(
  focusNode: nameFocus,
  textInputAction: TextInputAction.next,
  onFieldSubmitted: (_) {
    FocusScope.of(context).requestFocus(emailFocus);
  },
)



25. Dispose FocusNodes


Focus nodes are long-lived objects, so their lifecycle should be managed by the stateful widget that owns them. :contentReference[oaicite:10]{index=10}


@override
void dispose() {
  nameFocus.dispose();
  emailFocus.dispose();
  phoneFocus.dispose();
  super.dispose();
}



26. Input Formatting


Input formatters can be used to restrict the type or length of information entered into a field.


Numbers Only


import 'package:flutter/services.dart';

TextFormField(
  keyboardType: TextInputType.number,
  inputFormatters: [
    FilteringTextInputFormatter.digitsOnly,
  ],
  decoration: const InputDecoration(
    labelText: 'PIN Code',
  ),
)


Limit Characters


TextFormField(
  maxLength: 50,
  decoration: const InputDecoration(
    labelText: 'Username',
  ),
)



27. Dropdown in a Structured Form


Structured forms can contain selection fields as well as text fields.


String? selectedCourse;

DropdownButtonFormField(
  decoration: const InputDecoration(
    labelText: 'Select Course',
    border: OutlineInputBorder(),
  ),
  items: const [
    DropdownMenuItem(
      value: 'flutter',
      child: Text('Flutter Development'),
    ),
    DropdownMenuItem(
      value: 'web',
      child: Text('Web Development'),
    ),
    DropdownMenuItem(
      value: 'python',
      child: Text('Python Development'),
    ),
  ],
  onChanged: (value) {
    selectedCourse = value;
  },
  validator: (value) {
    if (value == null) {
      return 'Please select a course';
    }
    return null;
  },
)


Flutter's FormField system is also used by widgets such as DropdownButtonFormField. :contentReference[oaicite:11]{index=11}




28. Checkbox in a Structured Form


Checkboxes can be used to collect boolean choices such as accepting terms and conditions.


bool acceptedTerms = false;

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


Validate Checkbox


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



29. Radio Buttons in a Structured Form


String? gender;

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




30. Date Selection in a Form


Date fields are useful for date of birth, appointment dates, delivery dates, and similar information.


DateTime? selectedDate;

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

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


Open Date Picker


ElevatedButton(
  onPressed: () {
    selectDate(context);
  },
  child: const Text('Select Date'),
)



31. Password Visibility


A structured account form often includes password fields. Password visibility can be controlled using a boolean state variable.


bool passwordVisible = false;

TextFormField(
  obscureText: !passwordVisible,
  decoration: InputDecoration(
    labelText: 'Password',
    border: const OutlineInputBorder(),
    suffixIcon: IconButton(
      icon: Icon(
        passwordVisible
            ? Icons.visibility
            : Icons.visibility_off,
      ),
      onPressed: () {
        setState(() {
          passwordVisible = !passwordVisible;
        });
      },
    ),
  ),
)




32. Structuring Forms with Helper Widgets


Very large forms can become difficult to maintain if every field is written directly inside one large build() method. Reusable helper widgets can improve readability.


Reusable Input Widget


Widget buildInputField({
  required String label,
  required TextEditingController controller,
  TextInputType? keyboardType,
}) {
  return TextFormField(
    controller: controller,
    keyboardType: keyboardType,
    decoration: InputDecoration(
      labelText: label,
      border: const OutlineInputBorder(),
    ),
    validator: (value) {
      if (value == null || value.trim().isEmpty) {
        return '$label is required';
      }
      return null;
    },
  );
}

Using the Helper


Column(
  children: [
    buildInputField(
      label: 'Name',
      controller: nameController,
    ),
    const SizedBox(height: 16),
    buildInputField(
      label: 'Email',
      controller: emailController,
      keyboardType: TextInputType.emailAddress,
    ),
  ],
)



33. Separating Form Sections


For large applications, each logical section can be implemented as a separate widget.


Column(
  children: [
    PersonalInformationSection(),
    const SizedBox(height: 24),
    ContactInformationSection(),
    const SizedBox(height: 24),
    AccountInformationSection(),
    const SizedBox(height: 24),
    TermsSection(),
  ],
)

This approach makes large forms easier to read, test, reuse, and maintain.




34. Complete Structured Registration Form


import 'package:flutter/material.dart';
import 'package:flutter/services.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();
  final confirmPasswordController = TextEditingController();

  String? selectedCourse;
  bool acceptedTerms = false;

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

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

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

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

    print('Name: $name');
    print('Email: $email');
    print('Phone: $phone');
    print('Course: $selectedCourse');

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Registration successful'),
      ),
    );
  }

  void resetForm() {
    formKey.currentState!.reset();

    nameController.clear();
    emailController.clear();
    phoneController.clear();
    passwordController.clear();
    confirmPasswordController.clear();

    setState(() {
      selectedCourse = null;
      acceptedTerms = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Student Registration'),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(20),
          child: Form(
            key: formKey,
            child: Column(
              crossAxisAlignment:
                  CrossAxisAlignment.start,
              children: [
                const Text(
                  'Personal Information',
                  style: TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 16),
                TextFormField(
                  controller: nameController,
                  textCapitalization:
                      TextCapitalization.words,
                  decoration: const InputDecoration(
                    labelText: 'Full Name',
                    border: OutlineInputBorder(),
                  ),
                  validator: (value) {
                    if (value == null ||
                        value.trim().isEmpty) {
                      return 'Enter your full 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 'Enter your email';
                    }

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

                    return null;
                  },
                ),
                const SizedBox(height: 16),
                TextFormField(
                  controller: phoneController,
                  keyboardType: TextInputType.phone,
                  inputFormatters: [
                    FilteringTextInputFormatter.digitsOnly,
                  ],
                  decoration: const InputDecoration(
                    labelText: 'Phone Number',
                    border: OutlineInputBorder(),
                  ),
                  validator: (value) {
                    if (value == null ||
                        value.trim().isEmpty) {
                      return 'Enter your phone number';
                    }

                    if (value.trim().length != 10) {
                      return 'Enter a 10-digit phone number';
                    }

                    return null;
                  },
                ),
                const SizedBox(height: 30),
                const Text(
                  'Course Information',
                  style: TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 16),
                DropdownButtonFormField(
                  decoration: const InputDecoration(
                    labelText: 'Select Course',
                    border: OutlineInputBorder(),
                  ),
                  items: const [
                    DropdownMenuItem(
                      value: 'flutter',
                      child: Text('Flutter Development'),
                    ),
                    DropdownMenuItem(
                      value: 'web',
                      child: Text('Web Development'),
                    ),
                    DropdownMenuItem(
                      value: 'python',
                      child: Text('Python Development'),
                    ),
                  ],
                  onChanged: (value) {
                    setState(() {
                      selectedCourse = value;
                    });
                  },
                  validator: (value) {
                    if (value == null) {
                      return 'Please select a course';
                    }
                    return null;
                  },
                ),
                const SizedBox(height: 30),
                const Text(
                  'Account Information',
                  style: TextStyle(
                    fontSize: 22,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                const SizedBox(height: 16),
                TextFormField(
                  controller: passwordController,
                  obscureText: true,
                  decoration: const InputDecoration(
                    labelText: 'Password',
                    border: OutlineInputBorder(),
                  ),
                  validator: (value) {
                    if (value == null ||
                        value.isEmpty) {
                      return 'Enter a password';
                    }

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

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

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

                    return null;
                  },
                ),
                const SizedBox(height: 20),
                CheckboxListTile(
                  contentPadding: EdgeInsets.zero,
                  value: acceptedTerms,
                  title: const Text(
                    'I agree to the Terms and Conditions',
                  ),
                  onChanged: (value) {
                    setState(() {
                      acceptedTerms = value ?? false;
                    });
                  },
                ),
                const SizedBox(height: 20),
                SizedBox(
                  width: double.infinity,
                  child: ElevatedButton(
                    onPressed: submitForm,
                    child: const Text('Register'),
                  ),
                ),
                const SizedBox(height: 10),
                SizedBox(
                  width: double.infinity,
                  child: OutlinedButton(
                    onPressed: resetForm,
                    child: const Text('Reset'),
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}




35. Making Forms Scrollable


Large forms may contain more fields than can fit on the screen. Wrapping the form inside SingleChildScrollView allows users to scroll through all fields.


SingleChildScrollView(
  padding: const EdgeInsets.all(20),
  child: Form(
    key: formKey,
    child: Column(
      children: [
        TextFormField(),
        TextFormField(),
        TextFormField(),
        TextFormField(),
        TextFormField(),
      ],
    ),
  ),
)

This is particularly useful on mobile devices when the on-screen keyboard reduces the available height.




36. Responsive Structured Forms


A form should be comfortable to use on mobile, tablet, and desktop screens. On larger screens, the form can be given a maximum width.


Center(
  child: ConstrainedBox(
    constraints: const BoxConstraints(
      maxWidth: 600,
    ),
    child: Padding(
      padding: const EdgeInsets.all(20),
      child: Form(
        key: formKey,
        child: Column(
          children: [
            TextFormField(),
            const SizedBox(height: 16),
            TextFormField(),
          ],
        ),
      ),
    ),
  ),
)



37. Two-Column Form Layout


On larger screens, related fields can be displayed side by side.


Row(
  children: [
    Expanded(
      child: TextFormField(
        decoration: const InputDecoration(
          labelText: 'First Name',
        ),
      ),
    ),
    const SizedBox(width: 16),
    Expanded(
      child: TextFormField(
        decoration: const InputDecoration(
          labelText: 'Last Name',
        ),
      ),
    ),
  ],
)

For small screens, the fields can be stacked vertically instead.




38. Form Data Model


For larger applications, form data can be represented using a Dart model class instead of passing many individual variables around.


class Student {
  final String name;
  final String email;
  final String phone;
  final String course;

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


Creating the Model from Form Data


final student = Student(
  name: nameController.text.trim(),
  email: emailController.text.trim(),
  phone: phoneController.text.trim(),
  course: selectedCourse!,
);



39. Submitting Structured Form Data


After validation, the collected data can be passed to a service, repository, API client, or database layer.


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

  final student = Student(
    name: nameController.text.trim(),
    email: emailController.text.trim(),
    phone: phoneController.text.trim(),
    course: selectedCourse!,
  );

  saveStudent(student);
}

void saveStudent(Student student) {
  print(student.name);
  print(student.email);
  print(student.phone);
  print(student.course);
}




40. Loading State During Submission


If submitting the form requires an asynchronous API call, use a loading state to prevent repeated submissions.


bool isLoading = false;

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

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

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

    print('Data submitted successfully');
  } finally {
    if (mounted) {
      setState(() {
        isLoading = false;
      });
    }
  }
}


Submit Button


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



41. Structured Form Submission Flow


User enters information
        ↓
Input fields collect values
        ↓
Form validation starts
        ↓
Are all fields valid?
     ↙          ↘
   No            Yes
   ↓              ↓
Show errors     Collect data
                  ↓
             Create model
                  ↓
              API request
                  ↓
          Success / Failure
                  ↓
           Show result



42. Handling Server-Side Errors


Client-side validation is useful for user experience, but applications should also handle errors returned by the backend.


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

  try {
    await saveDataToServer();

    if (!mounted) return;

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Data saved successfully'),
      ),
    );
  } catch (error) {
    if (!mounted) return;

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Unable to save data'),
      ),
    );
  }
}

Future saveDataToServer() async {
  await Future.delayed(
    const Duration(seconds: 1),
  );
}




43. Common Mistakes While Creating Structured Forms


Mistake 1: Recreating 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 instead.


Mistake 2: Forgetting Validation


Do not submit important data without checking whether required fields and formats are valid.


Mistake 3: Forgetting to Dispose Controllers


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

Mistake 4: Creating a Very Long Unstructured Form


Divide large forms into meaningful sections so users can understand what information is required.


Mistake 5: No Loading State


When an API request is running, protect the form from accidental repeated submissions.


Mistake 6: Poor Error Messages


Validation messages should clearly tell the user what needs to be corrected.




44. Best Practices for Structured Forms



  • Use a Form to group related form fields.

  • Use GlobalKey when direct form-state access is needed.

  • Use TextFormField for validated text input.

  • Keep form sections logically organized.

  • Use clear labels and hints.

  • Use appropriate keyboard types.

  • Validate required fields.

  • Use input formatters for input restrictions.

  • Use TextEditingController when direct programmatic control is required.

  • Dispose controllers and focus nodes owned by stateful widgets.

  • Use scrolling for long forms.

  • Use responsive layouts for different screen sizes.

  • Disable repeated submission while an asynchronous request is running.

  • Do not expose passwords or sensitive data through debug logs in production.

  • Perform appropriate server-side validation as well.




45. Interview Questions


Q1. What is a Form in Flutter?


A Form is a widget used to group and manage multiple form fields.


Q2. What is a structured form?


A structured form is a logically organized collection of related input fields, validation rules, actions, and data-processing logic.


Q3. What is GlobalKey?


It is a key commonly used to access the current state of a Form and perform operations such as validation, saving, and resetting.


Q4. What is TextFormField?


TextFormField is a form-aware text input widget that integrates a text field with Flutter's FormField system.


Q5. How do you validate a form?


if (formKey.currentState!.validate()) {
  // Form is valid.
}

Q6. What does validator return?


A validator returns an error message string when the field is invalid and null when the field is valid.


Q7. How do you save a form?


formKey.currentState!.save();

Q8. How do you reset a form?


formKey.currentState!.reset();

Q9. Why use TextEditingController?


It provides direct access to the current text and allows the application to read, change, or clear field values programmatically.


Q10. Why is focus management important?


It creates a smoother input flow by allowing users to move between fields efficiently, especially on mobile keyboards.




46. Practical Exercise


Create a complete Student Registration Application with a structured form containing:



  • First Name

  • Last Name

  • Email

  • Phone Number

  • Date of Birth

  • Gender

  • Course

  • Address

  • Password

  • Confirm Password

  • Terms and Conditions

  • Register Button

  • Reset Button


Requirements



  1. Use Form.

  2. Use GlobalKey.

  3. Use TextFormField for text input.

  4. Validate all required fields.

  5. Validate email format.

  6. Validate phone number.

  7. Validate password length.

  8. Confirm that both passwords match.

  9. Use a dropdown for course selection.

  10. Use a date picker for date of birth.

  11. Use radio buttons for gender.

  12. Use a checkbox for terms and conditions.

  13. Use TextEditingController where programmatic access is required.

  14. Use FocusNode for keyboard navigation.

  15. Make the form scrollable.

  16. Add a loading state during submission.

  17. Display a success message after successful submission.

  18. Provide a Reset button.

  19. Dispose controllers and focus nodes properly.




47. Quick Revision


















RequirementFlutter Feature
Group fieldsForm
Text inputTextFormField
Form field stateFormField
Access form stateGlobalKey
Validate formformKey.currentState!.validate()
Save formformKey.currentState!.save()
Reset formformKey.currentState!.reset()
Read textcontroller.text
Clear textcontroller.clear()
Control focusFocusNode
Restrict inputinputFormatters
Automatic validationautovalidateMode
Field validationvalidator
Save individual fieldonSaved



48. Key Takeaways



  • Structured forms organize related user input into a single manageable unit.

  • The Form widget acts as a container for multiple form fields.

  • TextFormField integrates text input with the form-field system.

  • GlobalKey provides convenient access to form state.

  • validate() checks all validators in the form.

  • save() invokes the fields' onSaved callbacks.

  • reset() resets form fields and validation state.

  • TextEditingController provides programmatic control over text fields.

  • FocusNode helps create smooth keyboard navigation.

  • Large forms should be divided into logical sections.

  • Long forms should generally support scrolling.

  • Responsive layouts should adapt to different screen sizes.

  • Validation should happen before data is processed or submitted.

  • Controllers and focus nodes should be disposed when they are no longer needed.




49. Official Flutter Resources





50. JustAcademy Flutter Training Resources


Learn more about Flutter development through the following resources:



whatsapp