Lewis Cianci I'm a passionate mobile-first developer, and I've been making apps with Flutter since it first released. I also use ASP.NET 5 for web. Given the chance, I'll talk to you for far too long about why I love Flutter so much.

Creating reactive forms in Flutter

8 min read 2468

Creating Reactive Forms In Flutter

In almost every app that you develop, sooner or later there will arise a need to capture user input. Fortunately, capturing text inputs is fairly simple within Flutter. However, as more fields and input types are added to a form, capturing this information rapidly increases in complexity.

Normally, these input fields, whether they are text fields, date fields, or any other type of input, are referred to as “controls.” Validation can also become an issue, as even simple validation for certain fields can require lengthy custom validators to be written.

In this article, we’ll create a registration form with input validation and fields that change based on the value of other fields. We’ll first accomplish this without using reactive forms, then reimplement the same form using reactive forms to understand the benefits of reactive forms in Flutter.

What we’ll cover:

Flutter reactive forms project overview

The app we will create is an enrollment app for pets into a “pet hotel” — a place where people can drop their pets off when they go on vacation.

In order for this app to work, people need to give details such as their name and phone number, what kind of pet they have, and their pet’s likes and dislikes. The end result will look like this:

User Shown Entering Pet Hotel App And Entering Information In Flutter Reactive Form

This form has a few requirements.

First, the three followup questions must change depending on what type of pet the user selects.

Next, the answers to those three questions are required, so we must add Flutter form validation logic to ensure they are filled out.



Finally, the phone number must only contain numbers, so if it contains non-numeric values, then the form should reject that entry and inform the user.

Making the form in Flutter without reactive forms

In this first approach, we’re manually creating the forms ourselves, and we would also like to capture the text inputs that are in these individual fields.

Because of this, we’re responsible for creating individual TextControllers that we can associate to the TextFormField widgets. We’re also responsible for creating a variable that will house the selected pet.

Let’s create those variables now:

final _formKey = GlobalKey<FormState>();
PetType? _petType;
final firstName = TextEditingController();
final lastName = TextEditingController();
final questionResponses = List.generate(3, (index) => TextEditingController());

To write text into these fields, we’ll create the TextFormField widgets and bind them to the appropriate controllers:

TextFormField(
  decoration: InputDecoration(hintText: 'First Name'),
  controller: firstName,
),
TextFormField(
  decoration: InputDecoration(hintText: 'Last Name'),
  controller: lastName,
),

The phone number input field is a little bit different, as we need to both validate that it has a valid phone number in it as well as prompt the user when invalid input is detected:

TextFormField(
  decoration: InputDecoration(hintText: 'Phone number'),
  autovalidateMode: AutovalidateMode.always,
  validator: (val) {
    if (val == null || val == "") {
      return 'Please enter a phone number';
    }
    if (int.tryParse(val) == null) {
      return 'Only enter numbers in the phone number field';
    }
    return null;
  },
),

Next, we specify the pet chooser. This is a RadioListTile that lets the user select what kind of pet they are bringing in: Cat, Dog, or Echidna.

When the user selects a type of pet, we also want to iterate through the previous answers given to these questions and clear them so that only one option is selected at a time.


More great articles from LogRocket:


RadioListTile<PetType>(
  value: PetType.cat,
  groupValue: _petType,
  onChanged: (val) => setState(() {
    for (final controller in questionResponses) {
      controller.clear();
    }
    _petType = val;
  }),
  title: Text('Cat'),
),

Finally, we want to change the questions that we are asking based on what type of pet has been selected.

We can achieve by using a Builder, that will update the widget tree depending on the value of a given variable. So, if the selected animal type is “Cat,” the form will display the questions for that animal type, and the same for animals of type Dog or Echidna.

Builder(
  builder: (context) {
    switch (_petType) {
      case PetType.cat:
        return Column(
          children: [
            Text("Aw, it's a cat!"),
            PetQuestionField(question: 'Can we pat the cat?', controller: questionResponses[0]),
            PetQuestionField(question: 'Can we put a little outfit on it?', controller: questionResponses[1]),
            PetQuestionField(question: 'Does it like to jump in boxes?', controller: questionResponses[2]),
          ],
        );

      case PetType.dog:
        return Column(
          children: [
            Text("Yay, a puppy! What's its details?"),
            PetQuestionField(question: 'Can we wash your dog?', controller: questionResponses[0]),
            PetQuestionField(question: 'What is your dog\'s favourite treat?', controller: questionResponses[1]),
            PetQuestionField(question: 'Is your dog okay with other dog\'s?', controller: questionResponses[2]),
          ],
        );

      case PetType.echidna:
        return Column(
          children: [
            Text("It's a small spiky boi. Can you fill us in on some of the details?"),
            PetQuestionField(question: 'How spikey is the echidna?', controller: questionResponses[0]),
            PetQuestionField(question: 'Can we read the echidna a story?', controller: questionResponses[1]),
            PetQuestionField(question: 'Does it like leafy greens?', controller: questionResponses[2]),
          ],
        );
      case null:
        {
          return Text('Please choose your pet type from above');
        }
    }
  },
),

With the individual form controls created, it’s time to create a button for the user to register their pet. This button should only allow the user to proceed if the supplied inputs are valid, and should prompt the user to correct any inputs that could not be validated.

ElevatedButton(
    onPressed: () {
      // Form is valid if the form controls are reporting that 
      // they are valid, and a pet type has been specified.
      final valid = (_formKey.currentState?.validate() ?? false) && _petType != null;
      if (!valid) {
      // If it's not valid, prompt the user to fix the form
        showDialog(
            context: context,
            builder: (context) => SimpleDialog(
                  contentPadding: EdgeInsets.all(20),
                  title: Text('Please check the form'),
                  children: [Text('Some details are missing or incorrect. Please check the details and try again.')],
                ));
      } else {
      // If it is valid, show the received values
        showDialog(
          context: context,
          builder: (context) => SimpleDialog(
            contentPadding: EdgeInsets.all(20),
            title: Text("All done!"),
            children: [
              Text(
                "Thanks for all the details! We're going to check your pet in with the following details.",
                style: Theme.of(context).textTheme.caption,
              ),
              Card(
                child: Column(
                  children: [
                    Text('First name: ${firstName.text}'),
                    Text('Last name: ${lastName.text}\r\n'),
                    Text('Pet type: ${_petType}'),
                    Text('Response 1: ${questionResponses[0].text}'),
                    Text('Response 2: ${questionResponses[1].text}'),
                    Text('Response 3: ${questionResponses[2].text}'),
                  ],
                ),
              )
            ],
          ),
        );
      }
    },
    child: Text('REGISTER'))

Issues with manually creating forms in Flutter

Using forms in Flutter isn’t unduly difficult, but hand-crafting our own forms can get a bit laborious. Let’s break down why that’s the case.

First, if you want to be able to get the text from a field or clear the field’s input, you have to create your own TextEditingController for each field. It’s easy to see how you could wind up with quite a few of these, which you would have to keep track of yourself.

Second, you have to write your own validation logic for simple things such as checking if a number is correct.

Finally, this approach results in quite a lot of boilerplate code. For one or two text fields, it’s not so bad, but it’s easy to see how it could scale poorly.

Two reactive form Flutter package options to consider

If we were to set off on a journey to find a package that would make this process easier, and we had “reactive forms” in mind, we would probably come across the reactive_formsFlutter package fairly quickly. And yet, it’s not the package that I would use to create reactive forms within my app.

Why not?

Well, the first sentence on pub.dev tells us that Reactive Forms is “… a model-driven approach to handling Forms inputs and validations, heavily inspired in Angular’s Reactive Forms.”

Due to this, we can establish that the mentality used in the reactive_forms package will be similar to what we find in Angular.

If we already know Angular, that’s possibly even more of a reason to use reactive_forms. But if we don’t know Angular, we’re more interested in the simplest way of achieving reactivity within our forms.

In my experience, I find using the package flutter_form_builder to be an easier, more extensible way of creating forms.

Of course, I encourage you to research both packages and choose the one that you prefer, as one package isn’t necessarily “better” than the other, but they do represent two different ways of achieving a similar result.

Using flutter_form_builder to create reactive forms

Now let’s use the package flutter_form_builder to create our forms. This can reduce the amount of code we have to write, make it easier to understand the code we’ve written, and also save us from writing our own validation logic.

First up, we’ll add a dependency to the flutter_form_builder package in our pubspec.yaml file:

flutter_form_builder: ^7.4.0

With that set up, let’s reimplement our forms to make use of flutter_form_builder.

We’ll need to add some names in for the fields that we intend to use within our form. We should set these to a variable name that is logical to us, as we’ll need to bind our FormBuilderTextField to them later on.

final String FIRST_NAME = 'FirstName';
final String LAST_NAME = 'LastName';
final String PHONE_NUMBER = 'PhoneNumber';
final String PET_CHOICE = 'PetChoice';
final String QUESTION_ANSWER_1 = 'QuestionAnswer1';
final String QUESTION_ANSWER_2 = 'QuestionAnswer2';
final String QUESTION_ANSWER_3 = 'QuestionAnswer3';

We also need to specify a GlobalKey<FormBuilderState>, to store the details that our form captures.

final _fbKey = GlobalKey<FormBuilderState>();

The next big change is that instead of our form being wrapped in a Form, we’ll wrap it in a FormBuilder, and specify a key for the FormBuilder.

FormBuilder(
  key: _fbKey,
  child: Column(children: [...children widgets here])
)

This means the FormBuilder will store values from the form in this key, so we can easily retrieve them later.

Setting up the basic form inputs

Normally, we would be responsible for manually specifying what TextEditingController should be used, as well as for setting up things like validation manually. But with flutter_form_builder, these two things become trivial.

For a text input field, we specify the name parameter of the field and, if we want to label the field, the decoration. We can also just choose from an existing set of validators instead of writing our own. This means that our first and last name input fields look like this:

FormBuilderTextField(
  name: FIRST_NAME,
  decoration: InputDecoration(labelText: 'First Name'),
  validator: FormBuilderValidators.required(),
),

For our phone number field, instead of writing our own validator, we can just leverage the FormBuilderValidators.numeric() validator:

FormBuilderTextField(
  name: PHONE_NUMBER,
  validator: FormBuilderValidators.numeric(),
  decoration: InputDecoration(labelText: 'Phone number'),
  autovalidateMode: AutovalidateMode.always,
),

Setting up the pet type chooser

Now we want to give the user a list of pet type options to choose from by selecting the appropriate radio button in our Flutter app. We can programmatically generate this list from our supplied set of enums.

This means that if we add or remove options from our enum within our program, the options will change within our form as well. This will be easier than manually maintaining the list ourselves.

FormBuilderRadioGroup<PetType>(
  onChanged: (val) {
    print(val);
    setState(() {
      _petType = val;
    });
  },
  name: PET_CHOICE,
  validator: FormBuilderValidators.required(),
  orientation: OptionsOrientation.vertical, // Lay out the options vertically
  options: [
    // Retrieve all options from the PetType enum and show them as options
    // Capitalize the first letters of the options as well
    ...PetType.values.map(
      (e) => FormBuilderFieldOption(
        value: e,
        child: Text(
          describeEnum(e).replaceFirst(
            describeEnum(e)[0],
            describeEnum(e)[0].toUpperCase(),
          ),
        ),
      ),
    ),
  ],
),

Setting up the three questions at the end

Our builder method remains largely the same for this part of our Flutter form, with a couple of important differences: we now use the FormBuilderTextField class for our inputs, and we associate them to the appropriate entry within the form via the name parameter.

case PetType.cat:
  return Column(
    children: [
      Text("Aw, it's a cat!"),
      FormBuilderTextField(
        name: QUESTION_ANSWER_1,
        decoration: InputDecoration(labelText: 'Can we pat the cat?'),
      ),
      FormBuilderTextField(
        name: QUESTION_ANSWER_2,
        decoration: InputDecoration(labelText: 'Can we put a little outfit on it?'),
      ),
      FormBuilderTextField(
        name: QUESTION_ANSWER_3,
        decoration: InputDecoration(labelText: 'Does it like to jump in boxes?'),
      ),
    ],
  );

Validating and retrieving values from the form

With our reactive Flutter form set up, there are two final things we need to do now: validate that the form has usable data in it and retrieve those values from the form.

Fortunately, because we’ve set the validation requirements within each field itself, our validation becomes quite simple:

final valid = _fbKey.currentState?.saveAndValidate() ?? false;

The result of this operation is that if the current state of our form is not null, and it is currently considered valid — that is, all form fields have passed validation — then, the form is considered valid. If currentState is null, or the form is invalid, this variable will instead return false.

In the case of a successful result, the values will be shown to the user. We can easily access the values within the form by accessing the currentState object within the _fbKey object.

showDialog(
  context: context,
  builder: (context) => SimpleDialog(
    contentPadding: EdgeInsets.all(20),
    title: Text("All done!"),
    children: [
      Text(
        "Thanks for all the details! We're going to check your pet in with the following details.",
        style: Theme.of(context).textTheme.caption,
      ),
      Card(
        child: Column(
          children: [
            // It's okay to use the ! operator with currentState, because we
            // already checked that it wasn't null when we did the form
            // validation
            Text('First name: ${_fbKey.currentState!.value[FIRST_NAME]}'),
            Text('Last name: ${_fbKey.currentState!.value[LAST_NAME]}'),
            Text('Number: ${_fbKey.currentState!.value[PHONE_NUMBER]}'),
            Text('Pet type: ${_fbKey.currentState!.value[PET_CHOICE]}'),
            Text('Response 1: ${_fbKey.currentState!.value[QUESTION_ANSWER_1]}'),
            Text('Response 2: ${_fbKey.currentState!.value[QUESTION_ANSWER_2]}'),
            Text('Response 3: ${_fbKey.currentState!.value[QUESTION_ANSWER_3]}'),
          ],
        ),
      )
    ],
  ),
);

Wrapping up

As we can see, using flutter_form_builder to create reactive forms in Flutter can lead to many improvements for us as developers. As always, you can browse this project’s code in Github to see how you can use flutter_form_builder in your project.

You can also use these links below to compare between two commits to see exactly how the project changed:

There are quite a few different types of fields that flutter_form_builder provides out of the box, so you should always be able to use the right field type for your need.

Have fun, and enjoy building those forms!

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Lewis Cianci I'm a passionate mobile-first developer, and I've been making apps with Flutter since it first released. I also use ASP.NET 5 for web. Given the chance, I'll talk to you for far too long about why I love Flutter so much.

Leave a Reply