We’ll be showing you how to implement and properly use the Flutter FormWidget
to create a simple or complex form. You’ll also find examples to better explain the concept of the widget.
As with HTML, you can live just fine without a Form widget. It is a convenience widget with no visual component. That is to say you never actually see it rendered on the device. Its only purpose is to wrap all of its inputs, thereby grouping them – and their data – into a unit. It does so using a key. This is one place where keys are needed. If you decide to use a Form, you need a GlobalKey of type FormState
:
GlobalKey<FormState> _key = GlobalKey<FormState>();
You’ll set that key as a property to your form:
@override Widget build(BuildContext context) { return Form( key: _key, autovalidate: true, child: // All the form fields will go here ); }
At first glance, the Form doesn’t seem to change anything. But a closer look reveals that we now have access to
autovalidate
: a bool. True means run validations as soon as any field changes. False means you’ll run it manually.- The key itself which we called
_key
in the preceding example
That _key has a currentState property which in turn has these methods:
save()
– Saves all fields inside the form by calling each’s onSavedvalidate()
– Runs each field’s validator functionreset()
– Resets each field inside the form back to its initialValue
Armed with all this, you can guess how the Form groups the fields nested inside of it. When you call one of these three methods on FormState, it iterates the inner fields and calls that method on each. One call at the Form level fires them all.
But hang on a second! If _key.currentState.save()
is calling a field’s onSaved()
, we need to provide an onSaved method. Same with validate()
calling the validator. But the TextField, Dropdown, Radio, Checkbox, and Slider widgets themselves don’t have those methods. What do we do now? We wrap each field in a FormField
widget which does have those methods. (And the rabbit hole gets deeper.).
FormField widget
This widget’s entire purpose in life is to provide save, reset, and validator event handlers to an inner widget. The FormField widget can wrap any widget using a builder property:
FormField<String>( builder: (FormFieldState<String> state) { return TextField(); // Any field widget like DropDownButton, // Radio, Checkbox, or Slider. }, onSaved: (String initialValue) { // Push values to a repository or something here. }, validator: (String val) { // Put validation logic here (further explained below). }, ),
So we first wrap a FormField widget around each input widget, and we do so in a method called the builder
. Then we can add the onSaved
and validator methods.
NOTE: Treat a TextField differently. Instead of wrapping it, replace it with a TextFormField widget if you use it inside a Form. This new widget is easy to confuse with a TextField but it is different. Basically …
TextFormField = TextField + FormField
The Flutter team knew that we’d routinely need a TextField widget in combination with a FormField widget so they created the TextFormField widget which has all of the properties of a TextField but adds an onSaved, validator, and reset:
TextFormField( onSaved: (String val) { print('Search Term TextField: form saved $val'); }, validator: (String val) { // Put your validation logic here }, ),
Now isn’t that nicer? Finally, we catch a break in making things easier. Checkboxes don’t have this feature. Nor do Radios nor Dropdowns. None except TextFields.
Best practice: Text inputs without a Form should always be a TextField. Text inputs inside a Form should always be a TextFormField.
onSaved
Please remember that your Form has a key which has a currentState
which has a save() method. Got all that? No? Not super clear? Let’s try it this way; on a “Save” button press, you will write your code to call …
_key.currentState.save();
… and it in turn invokes the onSaved method for each FormField that has one.
validator
Similarly, you probably guessed that you can call…
_key.currentState.validate();
… and Flutter will call each FormField’s validator method. But there’s more! If you set the Form’s autovalidate
property to true, Flutter will validate immediately as the user makes changes. Each validator function will receive a value – the value to be validated – and return a string. You’ll write it to return null if the input value is valid and an actual string if it is invalid. That returned string is the error message Flutter will show your user.
Validate while typing
Remember that the way to perform instant validation is to set Form.autovalidate to true and write a validator for your TextFormField
:
return Form( autovalidate: true, child: Container( TextFormField( validator: (String val) { // Let's say that an empty value is invalid. if (val.isEmpty) return 'We need something to search for'; return null; }, ), ), );
Obviously it makes no sense to validate a DropdownButton, Radio, Checkbox, Switch, or Slider while typing because you don’t type into them. But less obviously, it does not work with a TextField inside of a FormField. It only works with a TextFormField. Strange, right?
NOTE: Again, best practice is to use a TextFormField. But if you insist on using a TextField inside a FormField, you an brute force set errorText
like this:
FormField<String>( builder: (FormFieldState<String> state) { return TextField( controller: _emailController, decoration: InputDecoration( // This says if the value looks like an email set errorText // to null. If not, display an error message. errorText: RegExp(r'^[a-zA-Z0-9.][email protected][a-zA-Z0-9]+\. [a-zA-Z]+') .hasMatch(_emailController.text) ? null : "That's not an email address", ), ); }, ),
Validate only after submit attempt
There are times when you don’t want your code to validate until the user has finished entering data. You should first set autovalidate
to false. Then call validate() in the button’s pressed event:
RaisedButton( child: const Text('Submit'), onPressed: () { // If every field passes validation, run their save methods. if (_key.currentState.validate()) { _key.currentState.save(); print('Successfully saved the state.') } }, )
One big Form example
I know, I know. This is pretty complex stuff. It might help to see these things in context – how they all fit together. Below you’ll find a fully commented example … a big example. But as big as it is, it was originally much larger. Please look at our online source code repository for the full example. Hopefully, they will help your understanding of how Form fields relate.
Let’s say that we wanted to create a scene for the user to submit a Google-like web search. We’ll give them a
TextFormField for the search String, a DropdownButton with the type of search, a checkbox to enable/disable safeSearch
, and a button to submit:
enum SearchType { web, image, news, shopping }
// This is a stateful widget. Don't worry about how it or
// the setState() calls work until
class ProperForm extends StatefulWidget {
@override
_ProperFormState createState() => _ProperFormState();
}
class _ProperFormState extends State<ProperForm> {
// A Map (aka. hash) to hold the data from the Form.
final Map<String, dynamic> _searchForm = <String, dynamic>{
'searchTerm': ",
'searchType': SearchType.web,
'safeSearchOn': true,
};
// The Flutter key to point to the Form
final GlobalKey<FormState> _key = GlobalKey();
@override
Widget build(BuildContext context) {
return Form(
key: _key,
// Make autovalidate true to validate on every keystroke. In
// this case we only want to validate on submit.
//autovalidate: true,
child: Container(
child: ListView(
children: <Widget>[
TextFormField(
initialValue: _searchForm['searchTerm'],
decoration: InputDecoration(
labelText: 'Search terms',
),
// On every keystroke, you can do something.
onChanged: (String val) {
setState(() => _searchForm['searchTerm'] = val);
},
// When the user submits, you could do something
// for this field
onSaved: (String val) { },
//Called when we "validate()". The val is the String
// in the text box.
//Note that it returns a String; null if validation passes
// and an error message if it fails for some reason.
validator: (String val) {
if (val.isEmpty) {
return 'We need something to search for';
}
return null;
},
),
FormField<SearchType>(
builder: (FormFieldState<SearchType> state) {
return DropdownButton<SearchType>(
value: _searchForm['searchType'],
items: const <DropdownMenuItem<SearchType>>[
DropdownMenuItem<SearchType>(
child: Text('Web'),
value: SearchType.web,
),
DropdownMenuItem<SearchType>(
child: Text('Image'),
value: SearchType.image,
),
DropdownMenuItem<SearchType>(
child: Text('News'),
value: SearchType.news,
),
DropdownMenuItem<SearchType>(
child: Text('Shopping'),
value: SearchType.shopping,
),
],
onChanged: (SearchType val) {
setState(() => _searchForm['searchType'] = val);
},
);
},
onSaved: (SearchType initialValue) {},
),
// Wrapping the Checkbox in a FormField so we can have an
// onSaved and a validator
FormField<bool>(
//initialValue: false, // Ignored for Checkboxes
builder: (FormFieldState<bool> state) {
return Row(
children: <Widget>[
Checkbox(
value: _searchForm['safeSearchOn'],
// Every time it changes, you can do something.
onChanged: (bool val) {
setState(() => _searchForm['safeSearchOn'] = val);
},
),
const Text('Safesearch on'),
],
);
},
// When the user saves, this is run
onSaved: (bool initialValue) {},
// No need for validation because it is a checkbox. But
// if you wanted it, put a validator function here.
),
// This is the 'Submit' button
RaisedButton(
child: const Text('Submit'),
onPressed: () {
// If every field passes validation, let them through.
// Remember, this calls the validator on all fields in
// the form.
if (_key.currentState.validate()) {
// Similarly this calls onSaved() for all fields
_key.currentState.save();
// You'd save the data to a database or whatever here
print('Successfully saved the state.');
}
},
)
],
),
),
);
}
}
Hope you understand form better now