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:
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) , ),
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.
More great articles from LogRocket:
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.
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), PetQuestionField(question: 'Can we put a little outfit on it?', controller: questionResponses), PetQuestionField(question: 'Does it like to jump in boxes?', controller: questionResponses), ], ); case PetType.dog: return Column( children: [ Text("Yay, a puppy! What's its details?"), PetQuestionField(question: 'Can we wash your dog?', controller: questionResponses), PetQuestionField(question: 'What is your dog\'s favourite treat?', controller: questionResponses), PetQuestionField(question: 'Is your dog okay with other dog\'s?', controller: questionResponses), ], ); 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), PetQuestionField(question: 'Can we read the echidna a story?', controller: questionResponses), PetQuestionField(question: 'Does it like leafy greens?', controller: questionResponses), ], ); 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.text'), Text('Response 2: $questionResponses.text'), Text('Response 3: $questionResponses.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.
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.
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
With that set up, let’s reimplement our forms to make use of
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( 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
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), describeEnum(e).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
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
null, or the form is
invalid, this variable will instead return
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
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]'), ], ), ) ], ), );
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!
LogRocket: 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.
Try it for free.