Form Validation in Flutter with a Practical Example

In this Flutter tutorial, you will learn how to do basic form validation in Flutter. We will go through it step by step, and you’ll learn how to use Form and TextFormField widgets in Flutter.

If you are interested in Flutter video tutorials, check this playlist: Flutter Video Tutorials. Also, there is a large collection of code examples if you check Flutter tutorials page.

Creating a New Flutter App

First, we’ll create a new Flutter app to work with. This tutorial assumes you have Flutter installed. Open the terminal/command window on your computer and use the below Flutter command to create a new app.

flutter create form-validation

If all is good, the above command will create a new Flutter app for you. Open the created app project.

In the lib/main.dart filedelete everything, then copy and paste the below code snippet to make your app code a little bit simpler. This will be our starting point to work with.

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form validation',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Sign Up'),
    );
  }
}

Our next step will be to create MyHomePage widget:

  • It will be a stateful widget
  • If you are on VS Code, type stful shortcut to generate a stateful widget.
class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Container(
          padding: EdgeInsets.all(16),
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [],
          ),
        ),
      ),
    );
  }
}

Now, our starter code is ready!

TextFormField Widget

A TextFormField widget is the same as TextField, but with additional properties like validator and autovalidate.

A basic TextFormField widget should look like this:

TextFormField(
     controller: _controller,
     decoration: InputDecoration(
          hintText: 'Name',
     ),
),
  • We should provide a controller to it, to save any text entered by the user.
  • decoration property is used to customize the field. Using decoration property you can add hints, labels, icons, and so on.

Creating TextFormField Widgets

Now, we’ll create 4 TextFormFields:

  • Name
  • Email
  • Phone
  • Password

Later on, we’ll add validators specific to the type of field.

Let’s create our first TextFormField. This field is for the name.

TextFormField(
   autofillHints: [AutofillHints.name],
   decoration: InputDecoration(
      hintText: 'Name',
   ),
),

The above code snippet will create a TextFormField that will look like in the image below:

autofill-hints in flutter

 

  • autofillHints was added in Flutter 1.20+ and it displays autofill hints with the scopes provided as a list to it,
  • hintText shows the hint text for the field. It can be seen in the above image.

Similarly, we’ll create other text fields:

TextFormField(
  autofillHints: [AutofillHints.name],
  decoration: InputDecoration(
    hintText: 'Name',
  ),
),
SizedBox(
  height: 20,
),
TextFormField(
  autofillHints: [AutofillHints.email],
  decoration: InputDecoration(
    hintText: 'Email',
  ),
),
SizedBox(
  height: 20,
),
TextFormField(
  autofillHints: [AutofillHints.telephoneNumber],
  decoration: InputDecoration(
    hintText: 'Phone',
  ),
),
SizedBox(
  height: 20,
),
TextFormField(
  obscureText: true,
  decoration: InputDecoration(
    hintText: 'Password',
  ),
)

Note that we have used an additional property called obscureText for the password field, It will hide the password and show ‘*’ instead.

Now, your main.dart file should look like this:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form validation',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Sign Up'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextFormField(
              autofillHints: [AutofillHints.name],
              decoration: InputDecoration(
                hintText: 'Name',
              ),
            ),
            SizedBox(
              height: 20,
            ),
            TextFormField(
              autofillHints: [AutofillHints.email],
              decoration: InputDecoration(
                hintText: 'Email',
              ),
            ),
            SizedBox(
              height: 20,
            ),
            TextFormField(
              autofillHints: [AutofillHints.telephoneNumber],
              decoration: InputDecoration(
                hintText: 'Phone',
              ),
            ),
            SizedBox(
              height: 20,
            ),
            TextFormField(
              obscureText: true,
              decoration: InputDecoration(
                hintText: 'Password',
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Did we miss something though? Yes, you guessed it right! We haven’t created the controllers yet!

Creating Controllers

In this part, we’ll create TextEditingController for each TextFormField widget.

In the _MyHomePageState, we first declare variables for each controller.

TextEditingController _nameController;
TextEditingController _emailController;
TextEditingController _phoneController;
TextEditingController _passwordController;

and in the initState() function, we initialize all the controllers:

@override
void initState() {
  super.initState();
  _nameController = TextEditingController();
  _emailController = TextEditingController();
  _phoneController = TextEditingController();
  _passwordController = TextEditingController();
}

and set controllers to their respective textfield.

Form Validation

To enable field validation, we need to use the validator property in the TextFormField widget. The validator property will allow us to specify a function that takes a string parameter called value. It also returns a String.

validator: (String value) {
  return "";
},

Now we’ll go through some basic validators:

  1. Check if the provided value is empty

    validator: (value) {
      if (value.isEmpty) {
        return 'This field is required';
      }
    },
  2. Simple Email Address validators:

    a) To check if the email contains “@”:

    validator: (value) {
      if (value.isEmpty) {
        return 'This field is required';
      }
      if (!value.contains('@')) {
        return "A valid email address should contain '@'";
      }
    },

    b) A more comprehensive check of a proper email address through regexp:

    validator: (value) {
        if (value.isEmpty) {
          return 'This field is required';
        }
        if (!value.contains('@')) {
          return "A valid email should contain '@'";
        }
        if (!RegExp(
          r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+",
        ).hasMatch(value)) {
          return "Please enter a valid email";
        }
    },
  3.  Check the length of the provided value

    validator: (value) {
      if (value.isEmpty) {
        return 'This field is required';
      }
      if (value.length != 10) {
        return 'A valid phone number should be of 10 digits';
      }
    },
  4. Simple validator for the Password

    For password, we enforce length and a basic regexp that checks if it has letters and numbers

    validator: (value) {
          if (value.isEmpty) {
            return 'This field is required';
          }
          if (value.length < 8) {
            return 'Password should have atleast 8 characters';
          }
          if (!RegExp(r'[A-Z0-9a-z]*').hasMatch(value)) {
            return 'Enter a stronger password';
          }
    },

    This regular expression checks for capital letters (A-Z), numbers (0-9), and at least one special character.

Now, Your main.dart should look like this:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form validation',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Sign Up'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _nameController;
  TextEditingController _emailController;
  TextEditingController _phoneController;
  TextEditingController _passwordController;

  @override
  void initState() {
    super.initState();
    _nameController = TextEditingController();
    _emailController = TextEditingController();
    _phoneController = TextEditingController();
    _passwordController = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextFormField(
              controller: _nameController,
              autofillHints: [AutofillHints.name],
              decoration: InputDecoration(
                hintText: 'Name',
              ),
              validator: (value) {
                if (value.isEmpty) {
                  return 'This field is required';
                }
              },
            ),
            SizedBox(
              height: 20,
            ),
            TextFormField(
              controller: _emailController,
              autofillHints: [AutofillHints.email],
              decoration: InputDecoration(
                hintText: 'Email',
              ),
              validator: (value) {
                if (value.isEmpty) {
                  return 'This field is required';
                }
                if (!value.contains('@')) {
                  return "A valid email should contain '@'";
                }
                if (!RegExp(
                  r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+",
                ).hasMatch(value)) {
                  return "Please enter a valid email";
                }
              },
            ),
            SizedBox(
              height: 20,
            ),
            TextFormField(
              controller: _phoneController,
              autofillHints: [AutofillHints.telephoneNumber],
              decoration: InputDecoration(
                hintText: 'Phone',
              ),
              validator: (value) {
                if (value.isEmpty) {
                  return 'This field is required';
                }
                if (value.length != 10) {
                  return 'A valid phone number should be of 10 digits';
                }
              },
            ),
            SizedBox(
              height: 20,
            ),
            TextFormField(
              controller: _passwordController,
              obscureText: true,
              decoration: InputDecoration(
                hintText: 'Password',
              ),
              validator: (value) {
                if (value.isEmpty) {
                  return 'This field is required';
                }
                if (value.length < 8) {
                  return 'Password should have atleast 8 characters';
                }
                if (!RegExp(r'[A-Z0-9a-z]*').hasMatch(value)) {
                  return 'Enter a stronger password';
                }
              },
            ),
          ],
        ),
      ),
    );
  }
}

Final Steps

Finally, we just need to wrap our column with a Form widget. The Form widget takes a key parameter from which we can get the form’s state.

After the TextEditingController declaration, add the following line:

.
.
TextEditingController _passwordController;

final _formKey = GlobalKey<FormState>();

@override
void initState() {
.
.

Now, we add the _formKey to our Form widget.

body: Form(
        key: _formKey,
        child: Center(
          child: Column(
.
.
.

To check the form state, add a RaisedButton, and call the validate() method to see if all data is correct or not

RaisedButton(
  onPressed: () {
    if (!_formKey.currentState.validate()) {
      return;
    }
    //else make api call
    Scaffold.of(context).showSnackBar(
      SnackBar(
        content: Text('Looks Good!'),
      ),
    );
  },
  child: Text('SUBMIT'),
),

Complete Code Example

The final main.dart file should look like this:

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form validation',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Sign Up'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  TextEditingController _nameController;
  TextEditingController _emailController;
  TextEditingController _phoneController;
  TextEditingController _passwordController;

  final _formKey = GlobalKey<FormState>();

  @override
  void initState() {
    super.initState();
    _nameController = TextEditingController();
    _emailController = TextEditingController();
    _phoneController = TextEditingController();
    _passwordController = TextEditingController();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Form(
        key: _formKey,
        child: Container(
          padding: EdgeInsets.all(16),
          child: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                TextFormField(
                  controller: _nameController,
                  autofillHints: [AutofillHints.name],
                  decoration: InputDecoration(
                    hintText: 'Name',
                  ),
                  validator: (value) {
                    if (value.isEmpty) {
                      return 'This field is required';
                    }
                  },
                ),
                SizedBox(
                  height: 20,
                ),
                TextFormField(
                  controller: _emailController,
                  autofillHints: [AutofillHints.email],
                  decoration: InputDecoration(
                    hintText: 'Email',
                  ),
                  validator: (value) {
                    if (value.isEmpty) {
                      return 'This field is required';
                    }
                    if (!value.contains('@')) {
                      return "A valid email should contain '@'";
                    }
                    if (!RegExp(
                      r"^[a-zA-Z0-9.a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]+@[a-zA-Z0-9]+\.[a-zA-Z]+",
                    ).hasMatch(value)) {
                      return "Please enter a valid email";
                    }
                  },
                ),
                SizedBox(
                  height: 20,
                ),
                TextFormField(
                  controller: _phoneController,
                  autofillHints: [AutofillHints.telephoneNumber],
                  decoration: InputDecoration(
                    hintText: 'Phone',
                  ),
                  validator: (value) {
                    if (value.isEmpty) {
                      return 'This field is required';
                    }
                    if (value.length != 10) {
                      return 'A valid phone number should be of 10 digits';
                    }
                  },
                ),
                SizedBox(
                  height: 20,
                ),
                TextFormField(
                  controller: _passwordController,
                  obscureText: true,
                  decoration: InputDecoration(
                    hintText: 'Password',
                  ),
                  validator: (value) {
                    if (value.isEmpty) {
                      return 'This field is required';
                    }
                    if (value.length < 8) {
                      return 'Password should have atleast 8 characters';
                    }
                  },
                ),
                SizedBox(
                  height: 20,
                ),
                RaisedButton(
                  onPressed: () {
                    if (!_formKey.currentState.validate()) {
                      return;
                    }
                    //else make api call
                    showDialog(
                      context: context,
                      child: AlertDialog(
                        content: Text('Looks Good'),
                      ),
                    );
                  },
                  child: Text('SUBMIT'),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

If you run the above code, you should get an app that looks and works as illustrated with the images below.

Initial view with no values provided

 

All inputs are invalid 

 

Phone number and password are invalid

 

All inputs are valid

I hope this Flutter tutorial was of good value to you. If you are interested in learning Flutter, please check other Flutter tutorials on this site. Some of them have video tutorials included.

Happy learning!


Leave a Reply

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