How to validate forms and user input the easy way using Flutter

Written by Wilberforce Uwadiegwu.

Coming from an Android background, validating forms and user input requires lines upon lines of boilerplate code.

First, you have to get a reference to the EditText (or any View for that matter). Except if you are using Kotlin Android Extensions, you have to resort to the good old findViewById() or @BindView — for those using Jake Wharton’s ButterKnife. After which, you get a reference to the text and do any validation on it.

Now imagine doing that for a UI that requires 10 different types of user input. You practically have to write a jungle of nested if-else statements.

How is User Input Validation Handled in Flutter?

Now, Flutter comes with a very simple form validation API. It just requires the use of Form and TextFormField widgets.

Form is a container for FormFields (and its descendants) and other widgets. We also have save, validate, and reset functions. These facilitate easier validation and manipulation of data inputted in one or multiple FormFields.

TextFormField is just a FormField that contains a TextField. It has onSaved and validator optional typedef parameters. Both functions are called when save and validate are respectively called on the container Form.

How to Validate Forms and User Inputs in Flutter.

So, let’s dive in into the actual code…

final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
String email;
String password;
bool _autoValidate = false;
...
new Form(
key: _formKey,
autovalidate: _autoValidate,
child: new Column(
children: <Widget>[
new SizedBox(
height: 20.0,
),
new TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
onSaved: (String value) {
email = value;
},
validator: _validateEmail,
keyboardType: TextInputType.emailAddress,
),
new SizedBox(
height: 20.0,
),
new TextFormField(
decoration: const InputDecoration(labelText: 'Password'),
onSaved: (String value) {
password = value;
},
validator: _validatePassword,
obscureText: true,
),
new SizedBox(
height: 20.0,
),
new RaisedButton(
onPressed: _validateInputs,
child: new Text('Login'),
)
],
))

I’ll explain the code in snippets below:

new Form(
key: _formKey,
autovalidate: _autoValidate,

Here, we are creating a container for FormFields. While the key property is for assessing the current state of the Widget, the autovalidate property is to toggle auto validation of all the FormFields in this Form container. When autovalidate is set to true, the validate function of this Form is called each time a character is entered in any of the FormFields.

new TextFormField(
decoration: const InputDecoration(labelText: 'Email'),
validator: _validateEmail,
onSaved: (String value) {
email = value;
},
keyboardType: TextInputType.emailAddress,
)

In the above code snippet, we’re simply instantiating a TextFormField. The actual validation happens in the validator property. We either return null or an error string. Returning null means that the value entered by the user is OK and conforms with the format that is expected. If we return an error string, that means that something is wrong with the user input. This should be user-readable as it is what is displayed to the user.

This is how the email validation code looks:

String _validateEmail(String value) {
if (value.isEmpty) {
// The form is empty
return "Enter email address";
}
// This is just a regular expression for email addresses
String p = "[a-zA-Z0-9\+\.\_\%\-\+]{1,256}" +
"\\@" +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
"(" +
"\\." +
"[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
")+";
RegExp regExp = new RegExp(p);

if (regExp.hasMatch(value)) {
// So, the email is valid
return null;
}

// The pattern of the email didn't match the regex above.
return 'Email is not valid';
}

After validating the Form, we call save on it, and this will in turn call onSaved of all the containing TextFormFields. In onSaved, we simply assign the value to the email variable.

new RaisedButton(
onPressed: _validateInputs,
child: new Text('Login'),
)

This is just a button that calls _validateInputs function when pressed.

void _validateInputs() {
final form = _formKey.currentState;
if (form.validate()) {
// Text forms was validated.
form.save();
} else {
setState(() => _autoValidate = true);
}
}

First, we got the current state of the Form with its key, and we called validate on it. Just as I have said before, calling validate calls the validator function of all the FormFields in this Form. Now, validate returns true if the validator function of all the FormFields returns null, otherwise, it returns false.

We also called save on the Form after validating all user inputs. Again, this calls onSaved of individual FormFields.

You noticed that we set _autoValidate to true, right? This is because we want the Form to start auto-validating after the validation fails the first time.

So that’s the way it works.

What about Check Boxes and Radio Buttons?

Well, there might be a cleaner way to do this. But the only way I could think of was an “Android-like” validation on the check boxes and radio buttons.

First, I created the Widget:

...
bool _termsChecked = false;
int radioValue = -1;
...
new Column(
children: <Widget>[
new RadioListTile<int>(
title: new Text('Male'),
value: 0,
groupValue: radioValue,
onChanged: handleRadioValueChange),
new RadioListTile<int>(
title: new Text('Female'),
value: 1,
groupValue: radioValue,
onChanged: handleRadioValueChange),
new RadioListTile<int>(
title: new Text('Transgender'),
value: 2,
groupValue: radioValue,
onChanged: handleRadioValueChange),
],
),
new SizedBox(
height: 20.0,
),
new CheckboxListTile(
title: new Text('Terms and Conditionns'),
value: _termsChecked,
onChanged: (bool value) =>
setState(() => _termsChecked = value)),

Then I made changes to my _validateInputs function.

void _validateInputs() {
final form = _formKey.currentState;
if (form.validate()) {
// Text forms has validated.
// Let's validate radios and checkbox
if (radioValue < 0) {
// None of the radio buttons was selected
_showSnackBar('Please select your gender');
} else if (!_termsChecked) {
// The checkbox wasn't checked
_showSnackBar("Please accept our terms");
} else {
// Every of the data in the form are valid at this point
form.save();
}
} else {
setState(() => _autoValidate = true);
}
}

So that’s it. If you have a cleaner way to validate check boxes and radio buttons, please let me know.

The full working code can be found in the repo here.


How to validate forms and user input the easy way using Flutter was originally published in freeCodeCamp on Medium.

2 Likes

Thank you so much! I was using two TextFormField’s and one’s validation depended on the other. Until I found your explanation, I didn’t realize I needed to wrap both of them in a Form() widget. duh!