How to Build Better Forms in Flutter

To get user data in Flutter, you can use the Form widget. While the Form widget is very flexible, it mainly supports text fields. Luckily, there is a package that provides many different types of fields, making it easy to build better forms in Flutter.

Create a Normal Form in Flutter

If you want to build a simple form in Flutter that only requires text fields, you can easily use Flutter’s Form widget along with the TextFormField widget. Here is a minimal example:

import 'package:flutter/material.dart';

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

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

  @override
  Widget build(BuildContext context) {
    final GlobalKey<FormState> formKey = GlobalKey();
    final emailController = TextEditingController();
    final passwordController = TextEditingController();

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Form(
            key: formKey,
            child: Column(
              children: [
                TextFormField(
                  controller: emailController,
                  decoration: const InputDecoration(
                    label: Text('Email'),
                  ),
                ),
                TextFormField(
                  controller: passwordController,
                  decoration: const InputDecoration(
                    label: Text('Password'),
                  ),
                  obscureText: true,
                ),
                const SizedBox(height: 10),
                ElevatedButton(
                  onPressed: () => print([
                    emailController.text,
                    passwordController.text,
                  ]),
                  child: const Text('Submit'),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

In the above example, we have our MyApp widget that displays a Scaffold widget. Inside the Scaffold widget we return a Form widget. The Form widget has a child attribute that can be used to add form fields. Because we want to have multiple fields and a button, we use the Column widget. Because the Column widget allows us to add multiple widgets.

In the children attribute of the Column widget we display multiple TextFormField widgets. These are Flutter’s built-in form text field widgets. The TextFormField widget takes a controller attribute that is used to save the input data. At last, we have an ElevatedButton widget that will print the input data to the terminal.

As you can see, the example above is a very minimal implementation. However, the Form widget is ideal for building forms with only text fields. If you want to learn more about it, feel free to check out the following article: How to Create a Login Screen in Flutter

Build Better Forms

As mentioned earlier, if we want to build a form with more than just text fields, we can use a package. The package we will be using is called Flutter Form Builder. It includes twelve different input fields and makes validating user input much simpler.

Installing Flutter Form Builder

To build better forms we need to install the following packages Flutter Form Builder and Form Builder Validators. The second package is only necessary for validation. We can install both packages by executing the following command inside our project:

flutter pub add flutter_form_builder form_builder_validators

Once the command is executed, make sure to check your pubspec.yaml file for the added dependencies. You should see the Flutter Form Builder and Form Builder Validators packages included in the dependencies section, like this:

dependencies:
  flutter_form_builder: ^9.3.0
  form_builder_validators: ^10.0.1

Create a Form in Flutter Using the Package

After installing the packages, we can start by creating a form using the FormBuilder class.

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';

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

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final GlobalKey<FormBuilderState> _formKey = GlobalKey<FormBuilderState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: FormBuilder(
            key: _formKey,
            child: Column(
              children: [
                FormBuilderTextField(
                  name: 'email',
                  decoration: const InputDecoration(labelText: 'Email'),
                ),
                const SizedBox(height: 10),
                FormBuilderDropdown(
                  name: 'gender',
                  decoration: const InputDecoration(labelText: 'Gender'),
                  items: ['Male', 'Female', 'Other']
                      .map(
                        (gender) => DropdownMenuItem(
                          value: gender,
                          child: Text(gender),
                        ),
                      )
                      .toList(),
                ),
                const SizedBox(height: 10),
                FormBuilderDateTimePicker(
                  name: 'birthdate',
                  decoration: const InputDecoration(labelText: 'Birthdate'),
                  inputType: InputType.date,
                  initialDate: DateTime.now(),
                  initialValue: DateTime.now(),
                  firstDate: DateTime(1900),
                  lastDate: DateTime.now(),
                ),
                const SizedBox(height: 10),
                ElevatedButton(
                  onPressed: () {
                    if (_formKey.currentState!.saveAndValidate()) {
                      print(_formKey.currentState!.value.entries.toList());
                    }
                  },
                  child: const Text('Submit'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

In the above code snippet, instead of using the Form widget we are now using the FormBuilder widget. Inside the FormBuilder widget we have three fields, FormBuilderTextField, FormBuilderDropdown, and FormBuilderDateTimePicker. Each form field has a name, which will be used to identify the field and retrieve its value.

When the user clicks the ElevatedButton, the form data is saved and validated using the _formKey.currentState!.saveAndValidate() method. However, we have not added any validation rules yet, we will do this in the upcoming section. At last, we print all the values from the Form to the terminal.

If we fill in the form and press submit we will get the following output:

[
  MapEntry(email: info@onlyflutter.com), 
  MapEntry(gender: Male), 
  MapEntry(birthdate: 2024-06-26 20:41:06.217114)
]

To retrieve the value of a single field, you can use the field name within square brackets as notation:

ElevatedButton(
  onPressed: () {
    if (_formKey.currentState!.saveAndValidate()) {
      print(_formKey.currentState!.value['email']);
    }
  },
  child: const Text('Submit'),
),

This will result in the following:

info@onlyflutter.com

Validation

Data validation is important when creating forms because it ensures that users provide the right Data validation is important when creating forms because it ensures that users provide the correct information. The Form Builder Validators package helps us set up validation rules. For example, we can ensure certain fields are filled, check that email addresses are in the correct format, and set limits on numbers.

import 'package:flutter/material.dart';
import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:form_builder_validators/form_builder_validators.dart';

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

class MyApp extends StatelessWidget {
  MyApp({super.key});

  final GlobalKey<FormBuilderState> _formKey = GlobalKey<FormBuilderState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(),
        body: Padding(
          padding: const EdgeInsets.all(16.0),
          child: FormBuilder(
            key: _formKey,
            child: Column(
              children: [
                FormBuilderTextField(
                  name: 'email',
                  decoration: const InputDecoration(labelText: 'Email'),
                  validator: FormBuilderValidators.compose([
                    FormBuilderValidators.required(),
                    FormBuilderValidators.email(),
                  ]),
                ),
                const SizedBox(height: 10),
                FormBuilderDropdown(
                  name: 'gender',
                  decoration: const InputDecoration(labelText: 'Gender'),
                  items: ['Male', 'Female', 'Other']
                      .map(
                        (gender) => DropdownMenuItem(
                          value: gender,
                          child: Text(gender),
                        ),
                      )
                      .toList(),
                  validator: FormBuilderValidators.compose([
                    FormBuilderValidators.required(),
                  ]),
                ),
                const SizedBox(height: 10),
                FormBuilderDateTimePicker(
                  name: 'birthdate',
                  decoration: const InputDecoration(labelText: 'Birthdate'),
                  inputType: InputType.date,
                  initialDate: DateTime.now(),
                  initialValue: DateTime.now(),
                  firstDate: DateTime(1900),
                  lastDate: DateTime.now(),
                  validator: FormBuilderValidators.compose([
                    FormBuilderValidators.required(),
                  ]),
                ),
                const SizedBox(height: 10),
                ElevatedButton(
                  onPressed: () {
                    if (_formKey.currentState!.saveAndValidate()) {
                      print(_formKey.currentState!.value.entries.toList());
                    }
                  },
                  child: const Text('Submit'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

In this code snippet, we added the validator property and used the FormBuilderValidators.compose function to set validations. The function takes a list of FormBuilderValidators:

  1. FormBuilderValidators.required: The field cannot be left empty.
  2. FormBuilderValidators.email: The entered text should be in a valid email format.

When the form is submitted, the FormBuilder widget automatically validates all the fields according to their validation rules. If the validation passes, the entered input is considered valid and can be accessed using _formKey.currentState!.value.entries.toList(). Otherwise, the user will see error messages displayed in the below screenshot.

Available fields

The Flutter Form Builder package provides a variety of pre-built form fields designed for different types of data and user interactions. These include checkboxes, dropdowns, date pickers, sliders, and more. These fields are highly flexible and can be customized to meet the specific needs of your application. Below is a list of all available fields along with an example:

1. FormBuilderCheckbox

Represents a single checkbox field.

FormBuilderCheckbox(
  name: 'checkbox_field',
  title: const Text('Checkbox'),
  initialValue: false,
  onChanged: (bool? value) {},
),

2. FormBuilderCheckboxGroup

Represents a list of checkboxes for multiple selections.

FormBuilderCheckboxGroup(
  name: 'checkbox_group_field',
  options: const [
    FormBuilderFieldOption(value: 'Option 1'),
    FormBuilderFieldOption(value: 'Option 2'),
    FormBuilderFieldOption(value: 'Option 3'),
  ],
  onChanged: (List<String>? values) {},
),

3. FormBuilderChoiceChip

Creates a chip that acts like a radio button for selecting one choice from a list.

FormBuilderChoiceChip(
  name: 'choice_chip_field',
  options: const [
    FormBuilderChipOption(value: 'Option 1'),
    FormBuilderChipOption(value: 'Option 2'),
    FormBuilderChipOption(value: 'Option 3'),
  ],
  onChanged: (String? value) {},
),

4. FormBuilderDateRangePicker

Allows users to select a range of dates.

FormBuilderDateRangePicker(
  name: 'date_range_field',
  decoration: const InputDecoration(label: Text('Date')),
  onChanged: (DateTimeRange? value) {},
  firstDate: DateTime.parse('2021-01-01'),
  lastDate: DateTime.parse('2024-06-25'),
),

5. FormBuilderDateTimePicker

Allows users to input Date, Time, or DateTime values.

FormBuilderDateTimePicker(
  name: 'date_time_field',
  inputType: InputType.date,
  initialDate: DateTime.now(),
  initialValue: DateTime.now(),
  firstDate: DateTime(1900),
  lastDate: DateTime.now(),
  onChanged: (DateTime? value) {},
),

6. FormBuilderDropdown

Presents a dropdown list for selecting a single value from a list.

FormBuilderDropdown(
  name: 'dropdown_field',
  items: ['Option 1', 'Option 2', 'Option 3']
      .map(
        (String option) => DropdownMenuItem(
          value: option,
          child: Text(option),
        ),
      )
      .toList(),
  onChanged: (String? value) {},
),

7. FormBuilderFilterChip

Creates a chip that acts as a checkbox for filtering purposes.

FormBuilderFilterChip(
  name: 'filter_chip_field',
  options: const [
    FormBuilderChipOption(value: 'Option 1'),
    FormBuilderChipOption(value: 'Option 2'),
    FormBuilderChipOption(value: 'Option 3'),
  ],
  onChanged: (List<String>? values) {},
),

8. FormBuilderRadioGroup

Allows users to select one value from a list of Radio Widgets.

FormBuilderRadioGroup(
  name: 'radio_group_field',
  options: const [
    FormBuilderFieldOption(value: 'Option 1'),
    FormBuilderFieldOption(value: 'Option 2'),
    FormBuilderFieldOption(value: 'Option 3'),
  ],
  onChanged: (String? value) {},
),

9. FormBuilderRangeSlider

Allows users to select a range from a range of values using a slider.

FormBuilderRangeSlider(
  name: 'range_slider_field',
  min: 0,
  max: 100,
  initialValue: const RangeValues(20, 80),
  onChanged: (RangeValues? values) {},
),

10. FormBuilderSlider

Represents a slider for selecting a double value.

FormBuilderSlider(
  name: 'slider_field',
  min: 0,
  max: 100,
  initialValue: 50,
  onChanged: (double? value) {},
),

11. FormBuilderSwitch

Provides a switch field.

FormBuilderSwitch(
  name: 'switch_field',
  initialValue: false,
  title: const Text('Switch'),
  onChanged: (bool? value) {},
),

12. FormBuilderTextField

Represents a regular text input field.

FormBuilderTextField(
  name: 'text_field',
  decoration: const InputDecoration(labelText: 'Text'),
  onChanged: (String? value) {},
),

Conclusion

The Flutter Form Builder package makes it very easy to create complex forms. This is because the package already comes with a lot of built-in fields and simplifies the validation. Not only that, it also removes much of the boilerplate that is normally needed when you use the regular form widget.

Tijn van den Eijnde
Tijn van den Eijnde
Articles: 40

Leave a Reply

Your email address will not be published. Required fields are marked *