Eshiet Ekemini A graduate of University of Uyo and a tech enthusiast, Ekemini has been building for mobile for two years, with a particular focus on Kotlin and Flutter.

Introduction to form validation in Flutter

7 min read 2154

Flutter Logo

Form validation is an integral part of most applications, and also an essential tool in the arsenal of any mobile application developer. With the advent of Flutter and its increasing popularity, we will explore how form validation works in Flutter and alternative ways to make it work better.

The aim of this article is to provide you with a broad grasp of how a neat and scalable implementation of form validation in Flutter works. At the end of this blog post, you will be able to apply the concepts to all of your future app development projects.

Getting started with form validation in Flutter

The Flutter SDK provides us with an out-of-the-box widget and functionalities to make our lives easier when using form validation. In this article, we’ll cover two approaches to form validation: the Form widget and the provider package. You can find more information on these two approaches in the official Flutter Docs.

Creating a form in Flutter

Form Validation

Empty Form

First, we are going to create a simple login page that has the following fields:

  • Email
  • Name
  • Phone number
  • Password

Form Being Filled Out

For the validation, we want the users of our app to fill invalid details in each of those fields so the logic will be defined as below.

  • Email: We want a valid email that contains some characters before the “@” sign, as well as the email domain at the end of the email.
  • Name: We want the user to enter a valid first name and last name, which can be accompanied by initials too.
  • Phone Number: For phone number validation, the user is expected to input 11 digits starting with the digit “0”.
  • Password: For our password validation, we expect the user to use a combination of an uppercase letter, a lowercase letter, a digit, and special character.

Only when the user’s input matches the above mentioned do we want to accept their input before making any requests, such as sending to a server or saving in a database.

Using Regex methods and Dart extension methods

In order to make our lives easier and to avoid writing multiple if-else statements, we are going to employ the use of Regex (Regular Expression) and Dart’s extension methods in our application.

We made a custom demo for .
No really. Click here to check it out.

Start by creating a new Flutter project in either of Vscode or Android Studio. You can check here to get started on creating a new Flutter project.

Replace the Flutter default counter application with your own stateful widget. You should have something like this:

import 'package:Flutter/material.dart';

import 'form/form_page.dart';

void main() {
  //runApp(Provider(create: (_) => FormProvider(), child: MyApp()));
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Form Validation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: FormPage(),
    );
  }
}

This is what our main.dart file looks like currently. Now, create a new dart file and name it form_page.dart, and create the FormPage stateful widget inside of it.

Let’s create an extension class that will contain all the extension methods we will be using for this tutorial.

extension extString on String {
  bool get isValidEmail {
    final emailRegExp = RegExp(r"^[a-zA-Z0-9.][email protected][a-zA-Z0-9]+\.[a-zA-Z]+");
    return emailRegExp.hasMatch(this);
  }

  bool get isValidName{
    final nameRegExp = new RegExp(r"^\s*([A-Za-z]{1,}([\.,] |[-']| ))+[A-Za-z]+\.?\s*$");
    return nameRegExp.hasMatch(this);
  }

  bool get isValidPassword{
    final passwordRegExp = new RegExp(r'^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[[email protected]#\$&*~]).{8,}$');
    return passwordRegExp.hasMatch(this);
  }

  bool get isNotNull{
    return this!=null;
}


  bool get isValidPhone{
    final phoneRegExp = RegExp(r"^\+?0[0-9]{10}$");
    return phoneRegExp.hasMatch(this);
  }


}

Note: For the scope of this article, we won’t spend much time elaborating on extension methods and how to construct Regex. If you are interested in learning more about extension methods in Dart, check here, and here’s what you need to know about constructing your own Regex.

You’ll notice our string extension contains five methods:

  • isValidEmail
  • isValidName
  • isValidPassword
  • isNotNull
  • isValidPhone

All the Regex methods above take the string and checks if it matches the Regex pattern, then returns true, or false if it doesn’t match. Now all we need to do is import this file into any of our files we need to use the extension methods.

Back to our FormPage() Widget. This is what our widget would look like.

 import 'package:Flutter/material.dart';
import 'package:form_validation_demo/core/extensions/extensions.dart';
import 'package:form_validation_demo/form/success.dart';

class FormPage extends StatefulWidget {
  @override
  _FormPageState createState() => _FormPageState();
}

class _FormPageState extends State<FormPage> {

//This key will be used to identify the state of the form.
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: SafeArea(
          child: Container(
            padding:EdgeInsets.all(20),
            child: Form(
            key: _formKey,
            child: Column(
            children: [
              _nameTextField(),
              _emailTextField(),
              _phoneTextField(),
              _passwordTextField(),
              _submitButton()
            ],
      ),
    ),
          ),
        ));
  }

  Widget _emailTFTextField() {
    return TextFormField(
      decoration: InputDecoration(hintText: 'Email'),
      validator: (v) {
        if (v.isValidEmail) {
          return null;
        } else {
          return 'Please enter a valid email';
        }
      },
    );
  }

  Widget _passwordTextField() {
    return TextFormField(
      obscureText: true,
      decoration: InputDecoration(hintText: 'Password'),
      validator: (v) {
        if (v.isValidPassword) {
          return null;
        } else {
          return 'Password must contain an uppercase, lowercase, numeric digit and special character';
        }
      },
    );
  }

  Widget _nameTextField() {
    return TextFormField(
      decoration: InputDecoration(hintText: 'Name'),
      validator: (v) {
        if (v.isValidName) {
          return null;
        } else {
          return 'Please enter a valid name';
        }
      },
    );
  }

  Widget _phoneTextField() {
    return TextFormField(
      decoration: InputDecoration(hintText: 'Phone'),
      validator: (v) {
        if (v.isValidPhone) {
          return null;
        } else {
          return 'Phone Number must be up to 11 digits';
        }
      },
    );
  }

  Widget _submitButton() {
    return Container(
      padding: EdgeInsets.all(20),
      child: ElevatedButton(
        child: Text('Submit'),
        onPressed: () {
          if(_formKey.currentState.validate()) {
            Navigator.push(
                context, MaterialPageRoute(builder: (_) => SuccessPage()));
          }
        } ,
      ),
    );
  }
}

Our widget tree is made up of: A Scaffold => SafeArea => Container => Form => Column.

We have created a formKey that will be added to our form widget to identify the state of our form, which is created by default in Flutter when using a form.

Inside our column, we have five functions that return a widget. The first four are our fields, while the last is the submit button.

Let’s take a careful look at one of the functions in our column.

  Widget _emailTextField() {
    return TextFormField(
      decoration: InputDecoration(hintText: 'Email'),
      validator: (v) {
        if (v.isValidEmail) {
          return null;
        } else {
          return 'Please enter a valid email';
        }
      },
    );
  }

emailTextField simply returns a TextFormField. The most important part of this is the validator field.

The validator field takes in the user input and checks to see if it satisfies our Regex condition. If it does, then the field returns null. If it doesn’t, it returns a string, which will be the error message shown on our text field.

We simply repeat this for our other input fields and use the matching extension methods from our extension class.

The way we trigger the validation on this page is by using the form key variable we created to give us access to the state of our form.

Widget _submitButton() {
    return Container(
      padding: EdgeInsets.all(20),
      child: ElevatedButton(
        child: Text('Submit'),
        onPressed: () {
          if(_formKey.currentState.validate()) {
            Navigator.push(
                context, MaterialPageRoute(builder: (_) => SuccessPage()));
          }
        } ,
      ),
    );
  }

So, whenever a user clicks on the button, we check _formKey.currentState.validate(),
then we carry out an action, which, in our case, would be simply navigating to a new screen.

Your success page can be anything or any screen you want to take the user to after completing the field validation and using the data entered by the user.

Form validation using Provider

Using Provider is another way to validate fields in Flutter. This technique is used mostly when I need to carry out some tasks on user input without cluttering the UI classes with codes. This is why we move the logic to our Provider class.

We’ll use the Provider package and add it to our pubspec.yaml file:

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.0
provider:

Our pubspec.yaml file should now look like the above, and we can proceed to run Flutter pub get to download the needed dependencies.

Then, we can create a new file called form_provider and create a class inside it that extends, ChangeNotifier. The ChangeNotifier class provides methods that enable us to listen to changes on objects we subscribe to using the ChangeNotifier.

This is why one of the most important methods provided by this class is notifylisteners()</code.

This method tells our listeners to get the latest values from objects or variables they subscribe to.

Before we move to creating our provider class, we are going to create a model that will have two variables, an error string and another string that we will call value for now.

class ValidationModel {
  String value;
  String error;

  ValidationModel(this.value, this.error);
}

In our Provider class, we are going to create four fields in our ValidationModel for the inputs we receive from the user: email, password, phone, and name of user. These fields will be private, so we will expose them using getters.

class FormProvider extends ChangeNotifier {
  ValidationModel _email = ValidationModel(null, null);
  ValidationModel _password = ValidationModel(null, null);
  ValidationModel _phone = ValidationModel(null, null);
  ValidationModel _name = ValidationModel(null, null);

  ValidationModel get email => _email;

  ValidationModel get password => _password;

  ValidationModel get phone => _phone;

  ValidationModel get name => _name;
}

Also, we create methods that are going to get the input from the text fields and validate them against our conditions.

If they meet our requirements, we return null for the ValidationModel error message, and if the user input doesn’t meet our criteria, we return the error message with the message we want displayed on our screen.

Finally, we will call notifylisteners and pass the getter to the error message fields in each of our text fields.

These methods will looks something like this:

String validateEmail(String val) {
  if (val.isValidEmail) {
    _email = ValidationModel(val, null);
  } else {
    _email = ValidationModel(null, 'Please Enter a Valid Email');
  }

  notifyListeners();
}

String validatePassword(String val) {
  if (val.isValidPassword) {
    _password = ValidationModel(val, null);
  } else {
    _password = ValidationModel(null,
        'Password must contain an uppercase, lowercase, numeric digit and special character');
  }
  notifyListeners();
}

String validateName(String val) {
  if (val.isValidName) {
    _name = ValidationModel(val, null);
  } else {
    _name = ValidationModel(null, 'Please enter a valid name');
  }

  notifyListeners();
}

String validatePhone(String val) {
  if (val.isValidPhone) {
    _phone = ValidationModel(val, null);
  } else {
    _phone = ValidationModel(null, 'Phone Number must be up to 11 digits');
  }
  notifyListeners();
}

bool get validate {
  if (_email.value.isNotNull &&
      _password.value.isNotNull &&
      _phone.value.isNotNull &&
      _name.value.isNotNull) {
    return true;
  } else {
    return false;
  }
}

Now, in our Provider class, we have one getter function called validate that will return true if all our validation conditions are met.

In our UI class, we will replace the previous code we had with something like this:

import 'package:Flutter/material.dart';
import 'package:form_validation_demo/form/success.dart';
import 'package:form_validation_demo/provider_validation/form_provider.dart';
import 'package:provider/provider.dart';

class ProviderFormPage extends StatefulWidget {
  @override
  _ProviderFormPageState createState() => _ProviderFormPageState();
}

class _ProviderFormPageState extends State<ProviderFormPage> {
  var _formProvider;

  @override
  Widget build(BuildContext context) {
    _formProvider = Provider.of<FormProvider>(context);
    return SafeArea(
      child: Scaffold(
        body: Container(
          padding: EdgeInsets.all(20),
          child: Form(
            child: Column(
              children: [
                TextFormField(
                  onChanged: (String val) {
                    _formProvider.validateName(val);
                  },
                  keyboardType: TextInputType.name,
                  decoration: InputDecoration(
                    errorText: _formProvider.name.error,
                    hintText: "Enter your Name",
                  ),
                ),
                TextFormField(
                  onChanged: (String val) {
                    _formProvider.validateEmail(val);
                  },
                  keyboardType: TextInputType.emailAddress,
                  decoration: InputDecoration(
                    errorText: _formProvider.email.error,
                    hintText: "Enter your Email",
                  ),
                ),
                TextFormField(
                  onChanged: (String val) {
                    _formProvider.validatePhone(val);
                  },
                  keyboardType: TextInputType.phone,
                  decoration: InputDecoration(
                    errorText: _formProvider.phone.error,
                    hintText: "Enter your phone Number",
                  ),
                ),
                TextFormField(
                  onChanged: (String val) {
                    _formProvider.validatePassword(val);
                  },
                  obscureText: true,
                  decoration: InputDecoration(
                    errorText: _formProvider.password.error,
                    hintText: "Enter your Password",
                  ),
                ),
                Consumer<FormProvider>(
                  builder: (context, model, child) => Container(
                    padding: EdgeInsets.all(20),
                    child: ElevatedButton(
                      child: Text('Submit'),
                      onPressed: (model.validate)
                          ? () {
                              Navigator.push(
                                  context,
                                  MaterialPageRoute(
                                      builder: (_) => SuccessPage()));
                            }
                          : null,
                    ),
                  ),
                )
              ],
            ),
          ),
        ),
      ),
    );
  }
}

Lastly, before using any Provider, we need to register it higher up our widget tree.

Let’s do this in our main.dart file.

void main() {
 runApp(ChangeNotifierProvider<FormProvider>(
create: (_) => FormProvider(), child: MyApp()));
}

Now we can proceed to run our application and see that we have similar results like the previous approach. The major reason to use the second approach — even if it looks like more work in terms of the lines of codes — is if you find yourself in a scenario where you would love to keep your UI code neat and tidy and void of all the data manipulations in your app.

Another perk of using the Provider approach is that it validates the user input while the user interacts with the text fields. This means the user doesn’t wait to click on the submit button before knowing if their input is valid or not.

Aside from this approach to validation of forms in UI, which is not totally new, there are still many other ways to validate a form. The bloc library also provides a Flutter package for validating fields — it is called form_bloc. You can check out the documentation here. Thanks so much for reading!

: Full visibility into your web 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.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Eshiet Ekemini A graduate of University of Uyo and a tech enthusiast, Ekemini has been building for mobile for two years, with a particular focus on Kotlin and Flutter.

Leave a Reply