DEV Community

Cover image for Flutter TextFormField Validation made easy
Aditya Subrahmanya Bhat
Aditya Subrahmanya Bhat

Posted on

Flutter TextFormField Validation made easy

Hey There!!! 👋

If you are a flutter developer and if you have created an app that requires an authentication page where the user enters his credentials , you would have used a TextFormField widget.

Now , let's say a user has entered his credentials (email, password, etc) and while doing so he accidentally enters an invalid email address ( for eg : " namegmail.com ") , there has to be some way to notify the user to enter the correct email address right?

Well yeah , there is something called validator property in the TextFormField widget.
We use it to specify the kind of input that is to be accepted.

The traditional way of doing it is to put an if-else conditional and validate the input.

validator:(val){
  if(val.isEmpty){
    return "Required";
  }
  if(val.length < 6){
    return "Password must be atleast 6 characters long";
  }
  if(val.length > 20){
    return "Password must be less than 20 characters";
  }
  if(!val.contains(RegExp(r'[0-9]'))){
    return "Password must contain a number";
  }
}
Enter fullscreen mode Exit fullscreen mode

A similar implementation can be done for email validation but the RegExp(regular expression) string would be different.

But if you're not a fan of if-else conditionals there's a similar way of handling validation using a package called form_field_validator

You can get the package from
https://pub.dev/packages/form_field_validator

My flutter and dart versions :

Flutter (Channel stable, 2.0.0, on Microsoft Windows [Version 10.0.19042.1110],
locale en-IN)

• Flutter version 2.0.0 at C:\src\flutter

• Framework revision 60bd88df91 (5 months ago), 2021-03-03 09:13:17 -0800

• Engine revision 40441def69

• Dart version 2.12.0

If you are using a different version , you might have to make some changes like adding null safety etc.

Getting Started

To use that package we need to create a new flutter project. To those out there using VScode , follow the below steps to create a new Flutter project :

  1. Open VScode and Ctrl+Shift+P and start typing Flutter

Screenshot (351).png

2.Click on Flutter:New Application Project and give it a name. I'll be naming it form_validator .

Screenshot (353).png

3.Just sit back and let the magic happen.

Flutter will go ahead and create a project for you with all the necessary files and folders in it.
This is how the main.dart file inside lib folder will look like .

main_initial.jpg

4.Now delete all the comments and unnecessary code and modify your main.dart to this :

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: LoginPage(),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

5.You'll get a red squiggly line under "LoginPage" and that's because it doesn't exist yet.

6.Now go to the lib folder and create a new file and name it loginPage.dart

7.Open loginPage.dart and paste the following code :

import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({ Key key }) : super(key: key);

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

class _LoginPageState extends State<LoginPage> {
  @override
  Widget build(BuildContext context) {
    return Container(

    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Paste the following after "
class _LoginPageState extends State {" :

  GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  TextEditingController _nameController= TextEditingController();
  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();
Enter fullscreen mode Exit fullscreen mode

Keys

What is a key? Every flutter widget has a key.These keys are helpful to flutter when the state changes . These keys help keep track of the widgets in the widget tree.

And yes keys are not necessary in stateless widgets because their state never changes and there is nothing to keep track of.Keys in flutter are of two types - LocalKey and GlobalKey.

GlobalKeys are useful when a widget's information is to be accessed somewhere else in the widget tree.

Form

We come across situations where we need to use multiple TextFormFields in our app. Wouldn't it be nice if there was a way to control all those TextFormFields in a single place? That's where Form comes into picture. With Form we can control all those fields and their data can be evaluated and validated together at one place.

_formKey is a GlobalKey to control the state of the Form(we will be implementing this later)

TextEditingController

_nameController is an object of class TextEditingController that gives us access to the input string typed in the "Name" input field. We can use it to notify the app about the changes made in the input field.

_emailController and _passwordController are objects of the same TextEditingController class with the same functionality.

We will create a boolean value called "obscure" to hide or make the password visible.
Let's set it to true initially.

import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({Key key}) : super(key: key);

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

class _LoginPageState extends State<LoginPage> {
  GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  TextEditingController _nameController = TextEditingController();
  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();
  bool obscure=true;
  @override
  Widget build(BuildContext context) {
    return Scaffold();
   }
}

Enter fullscreen mode Exit fullscreen mode

Let's go ahead and make a UI for the login page in our app.

import 'package:flutter/material.dart';

class LoginPage extends StatefulWidget {
  const LoginPage({Key key}) : super(key: key);

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

class _LoginPageState extends State<LoginPage> {
  GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  TextEditingController _nameController = TextEditingController();
  TextEditingController _emailController = TextEditingController();
  TextEditingController _passwordController = TextEditingController();
  bool obscure=true;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.grey[800],
      body: SingleChildScrollView(
        child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              SizedBox(
                height: 250.0,
              ),
              Container(
                margin: EdgeInsets.symmetric(horizontal: 30.0),
                decoration: BoxDecoration(
                  color: Colors.white,
                  borderRadius: BorderRadius.circular(20.0),
                ),
                child: Form(
                  key: _formKey,
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Padding(
                        padding: EdgeInsets.only(top: 20.0),
                        child: Text(
                          "Welcome",
                          style: TextStyle(
                            fontSize: 30.0,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.only(
                          top: 10.0,
                          left: 30.0,
                          right: 30.0,
                          bottom: 20.0,
                        ),
                        child: TextFormField(
                          controller: _nameController,
                          cursorColor: Colors.black,
                          style: TextStyle(),
                          decoration: InputDecoration(
                            labelText: 'Name',
                            labelStyle: TextStyle(color: Colors.black),
                            focusedBorder: UnderlineInputBorder(
                              borderSide: new BorderSide(color: Colors.black),
                            ),
                          ),
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.only(
                          left: 30.0,
                          right: 30.0,
                          bottom: 20.0,
                        ),
                        child: TextFormField(
                          controller: _emailController,
                          cursorColor: Colors.black,
                          style: TextStyle(),
                          decoration: InputDecoration(
                            labelText: 'Email',
                            labelStyle: TextStyle(color: Colors.black),
                            focusedBorder: UnderlineInputBorder(
                              borderSide: new BorderSide(color: Colors.black),
                            ),
                          ),
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.only(
                          left: 30.0,
                          right: 30.0,
                          bottom: 20.0,
                        ),
                        child: TextFormField(
                          obscureText: obscure,
                          controller: _passwordController,
                          cursorColor: Colors.black,
                          style: TextStyle(),
                          decoration: InputDecoration(
                suffixIcon: IconButton(
                              icon: Icon(
                                Icons.visibility,
                                color: Colors.black,
                              ),
                              onPressed: () {
                                setState(() {
                                  obscure = !obscure;
                                });
                              },
                            ),
                            labelText: 'Password',
                            labelStyle: TextStyle(color: Colors.black),
                            focusedBorder: UnderlineInputBorder(
                              borderSide: new BorderSide(color: Colors.black),
                            ),
                          ),
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.only(
                          left: 30.0,
                          right: 30.0,
                          bottom: 30.0,
                        ),
                        child: TextFormField(
                          obscureText: obscure,
                          cursorColor: Colors.black,
                          style: TextStyle(),
                          decoration: InputDecoration(
                            labelText: 'Re-type Password',
                            labelStyle: TextStyle(color: Colors.black),
                            focusedBorder: UnderlineInputBorder(
                              borderSide: new BorderSide(color: Colors.black),
                            ),
                          ),
                        ),
                      ),
                      Padding(
                        padding: EdgeInsets.only(bottom: 20.0),
                        child: ElevatedButton(
                          style: ButtonStyle(
                            backgroundColor: MaterialStateProperty.all<Color>(Colors.black),
                            shape: MaterialStateProperty.all<OutlinedBorder>(
                              RoundedRectangleBorder(
                                borderRadius: BorderRadius.circular(20.0),
                              ),
                            ),
                          ),
                          onPressed: () {},
                          child: Padding(
                            padding: EdgeInsets.symmetric(
                              horizontal: 60.0,
                            ),
                            child: Text("Submit"),
                          ),
                        ),
                      ),
                    ],
                  ),
                ),
              ),
              SizedBox(
                height: 100.0,
              ),
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

We've set the key property of Form widget to the _formKey and the controller properties of TextFormField widgets to their respective controllers.

Note that the underscores before the variables _formKey , _passwordController, etc mean that they are private to the LoginPage class and are only accessible inside this class.

8.Go to main.dart and import loginPage.dart

import 'package:form_validator/loginPage.dart';
Enter fullscreen mode Exit fullscreen mode

9.Go to pubspec.yaml file in your project and add the form_field_validator package

pubspec.jpg

10.Go to loginPage.dart and import the package

import 'package:form_field_validator/form_field_validator.dart';
Enter fullscreen mode Exit fullscreen mode

Validators

11.Now let's go to the TextFormFields and add the validators.

TextFormField(
 validator: RequiredValidator(errorText: "Required"),
 controller: _nameController,
 cursorColor: Colors.black,
 style: TextStyle(),
 decoration: InputDecoration(
 labelText: 'Name',
 labelStyle: TextStyle(color: Colors.black),
 focusedBorder: UnderlineInputBorder(
 borderSide: new BorderSide(color: Colors.black),
      ),
    ),
  ),
Enter fullscreen mode Exit fullscreen mode

The RequiredValidator takes in a single argument of type String for the errorText property and makes sure that the input field isn't empty . If it is empty , it displays the errorText specified.

Name input field may need only the RequiredValidator but what about email and password fields? They'll need multiple validators(required validation and valid email address validation and input length validation etc) as we saw in the traditional if-else conditionals right?

We can achieve that using MultiValidator that comes with the package

TextFormField(
 validator: MultiValidator([]),
 controller: _emailController,
 cursorColor: Colors.black,
 style: TextStyle(),
 decoration: InputDecoration(
 labelText: 'Email',
 labelStyle: TextStyle(color: Colors.black),
 focusedBorder: UnderlineInputBorder(
 borderSide: new BorderSide(color: Colors.black),
      ),
    ),
  ),
Enter fullscreen mode Exit fullscreen mode

MultiValidator takes a list of validators as arguments.

We add multiple validators to the email input field this way ...

validator: MultiValidator([
  RequiredValidator(errorText: "Required"),
  EmailValidator(errorText: "Please enter a valid email address"),
]),
Enter fullscreen mode Exit fullscreen mode

The EmailValidator takes in a single argument of type String for the errorText property and makes sure that the email address entered matches the regular expression - r"^((([a-z]|\d|[!#\$%&'*+-\/=\?^`{|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(.([a-z]|\d|[!#\$%&'*+-\/=\?^`{|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+))|((\x22)((((\x20|\x09)(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))(((\x20|\x09)(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|.||~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))).)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|.||~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$"

The regular expression looks scary right? Don't worry , just enter a valid email address and you'll be fine.

... and for the password input field :

validator: MultiValidator([
  RequiredValidator(errorText: "Required"),
  MinLengthValidator(6,
      errorText:
          "Password must contain atleast 6 characters"),
  MaxLengthValidator(15,
      errorText:
          "Password cannot be more 15 characters"),
  PatternValidator(r'(?=.*?[#?!@$%^&*-])',
      errorText:
          "Password must have atleast one special character"),
]),
Enter fullscreen mode Exit fullscreen mode

The MinLengthValidator takes 2 input arguments

1.The minimum length(int) of input.

2.String for the errorText property if the input doesn't meet the required length.

The MaxLengthValidator takes 2 input arguments

1.The maximum length(int) of input.

2.String for the errorText property if the input exceeds the required length.

The PatternValidator takes 2 input arguments

1.The pattern in the form of regular expression.

2.String for the errorText property if the input doesn't contain the regexp characters .

For the Re-type Password field :

validator: (val) {
  if (val.isEmpty) {
    return "Required";
  }
  return MatchValidator(
          errorText: "Passwords don't match")
      .validateMatch(val, _passwordController.text);
},
Enter fullscreen mode Exit fullscreen mode

If the input is empty , it returns the string "Required".

Else if the input isn't empty , it checks if the input matches the password entered earlier.

Note that we access the input entered in the fields using the text property of the controllers.

Finally we implement the onPressed function inside ElevatedButton :

onPressed: () {
   if (_formKey.currentState.validate()) {
   ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        backgroundColor: Colors.white,
        content: Text(
          'Validation Successful',
           style: TextStyle(
              color: Colors.black,
           ),
        ),
      ),
   );
  }
},
Enter fullscreen mode Exit fullscreen mode

Here we ask for the currentState of the form using _formKey and validate the form using the validate() function.

If the validate() function return true , a SnackBar is displayed.

Else , the repective errorText is displayed wherever the validation failed.

Run the App

It's time to see the code in action. Build the app on an emulator or your mobile phone.

Some Other Validators

The form_field_validator package comes with other validators :

  • RangeValidator()
    This function is used to specify the numeric range of the input value.

    It takes 3 named arguments -

    min - Minimum numeric value the input must have.

    max - Maximum numeric value the input can have.
    errorText - Text to be displayed if validation fails.

  • LengthRangeValidator()
    This function is like a combination of MinLengthValidator and MaxLengthValidator.It is used to specify the length of the input. It takes in 3 named arguments :

    min - Minimum length of input.

    max - Maximum length of the input.
    errorText - Text to be displayed if validation fails.

Next Steps

Full source code :

GitHub logo AdityaSubrahmanyaBhat / form

An app to demonstrate form validation

Well , That's all folks 😊 Thank you.

Top comments (2)

Collapse
 
silentroshi profile image
Kaushik

Very informative! Good work 👍🏻

Collapse
 
adityasubrahmanyabhat profile image
Aditya Subrahmanya Bhat

Thank you😊