How to add Validation to an Authentication Screen in Flutter

Flutter Jun 29, 2023

In web and mobile development, it is important to ensure the security and accuracy of user data. One way to do this is by validating the information users enter before proceeding. Validation helps us prevent errors and create a better user experience.

In this post, we will learn how to add validation to the authentication screen of a Flutter application. We will check if user inputs like email addresses and passwords are valid, and show error messages when needed. By the end, you will be able to apply validation to your own authentication screens in Flutter, improving the reliability and usability of your applications.

flutter_validation_messages_example

1. Setting up

Before we dive into the validation process, it is essential to understand the structure of the authentication screen we will be using. We will be building upon the authentication screen created in this previous post:

How to Create an Authentication Screen in Flutter
In this tutorial, we will learn how to create an authentication screen in Flutter. An authentication screen is an essential part of many mobile applications, as it allows users to sign in or register for an account. We will build a simple authentication screen with email and password fields

If you have not already, I encourage you to check out that post for a step-by-step tutorial on building an authentication screen with email and password fields.

This post will resume with the code from the post mentioned above. The code is also provided down below, if you want to follow along make sure you have the following directory structure and files in your project:

directory structure

|-- lib/
  |-- screens/
    |-- authentication_screen.dart 
  |-- widgets/
    |-- authentication_text_form_field.dart
    |-- wave.dart 
  |-- main.dart 

main.dart

import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/screens/authentication_screen.dart';

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

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Authentication Screen',
      theme: ThemeData(
        primaryColor: Colors.blueAccent,
        colorScheme: ThemeData().colorScheme.copyWith(
          primary: Colors.blueAccent,
        ),
      ),
      home: const AuthenticationScreen(),
    );
  }
}

authentication_screen.dart

import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/widgets/authentication_text_form_field.dart';
import 'package:flutter_authentication_screen/widgets/wave.dart';

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

  @override
  State<AuthenticationScreen> createState() => _AuthenticationScreenState();
}

class _AuthenticationScreenState extends State<AuthenticationScreen> {
  final GlobalKey<FormState> _formKey = GlobalKey();
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  final passwordConfirmationController = TextEditingController();
  bool register = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          const Wave(),
          Form(
            key: _formKey,
            child: Padding(
              padding: const EdgeInsets.only(left: 25.0, right: 25.0),
              child: Column(
                children: [
                  const SizedBox(height: 25),
                  AuthenticationTextFormField(
                    icon: Icons.email,
                    label: 'Email',
                    textEditingController: emailController,
                  ),
                  AuthenticationTextFormField(
                    icon: Icons.vpn_key,
                    label: 'Password',
                    textEditingController: passwordController,
                  ),
                  if (register == true)
                    AuthenticationTextFormField(
                      icon: Icons.password,
                      label: 'Password Confirmation',
                      textEditingController: passwordConfirmationController,
                    ),
                  const SizedBox(height: 25),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      minimumSize: const Size.fromHeight(50),
                      shape: const StadiumBorder(),
                    ),
                    onPressed: () => {},
                    child: Text(
                      register == true ? 'Register' : 'Login',
                      style: const TextStyle(fontSize: 17.5),
                    ),
                  ),
                  const SizedBox(height: 20),
                  InkWell(
                    onTap: () => setState(() {
                      register = !register;
                      _formKey.currentState?.reset();
                    }),
                    child: Text(
                      register == true ? 'Login instead' : 'Register instead',
                    ),
                  )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

authentication_text_form_field.dart

import 'package:flutter/material.dart';

class AuthenticationTextFormField extends StatelessWidget {
  const AuthenticationTextFormField({
    Key? key,
    required this.icon,
    required this.label,
    required this.textEditingController,
  }) : super(key: key);

  final IconData icon;
  final String label;
  final TextEditingController textEditingController;

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: textEditingController,
      obscureText: label.toLowerCase().contains('password'),
      decoration: InputDecoration(
        floatingLabelStyle: const TextStyle(fontSize: 20),
        icon: Icon(
          icon,
          color: Theme.of(context).primaryColor,
        ),
        labelText: label,
      ),
    );
  }
}

wave.dart

import 'package:flutter/material.dart';

class Wave extends StatelessWidget {
  const Wave({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ClipPath(
      clipper: WaveClipper(),
      child: Container(
        alignment: Alignment.center,
        color: Theme.of(context).primaryColor,
        height: 200,
      ),
    );
  }
}

class WaveClipper extends CustomClipper<Path> {
  @override
  getClip(Size size) {
    var path = Path();
    path.lineTo(0, 175);

    // The values of the calculations would be path.quadraticBezierTo(100, 75, 200, 150) if the height is 200 and the width is 400;
    path.quadraticBezierTo(size.width * 0.25, size.height * 0.50 - 25,
        size.width * 0.50, size.height * 0.75);

    // The values of the calculations would be path.quadraticBezierTo(300, 225, 400, 150) if the height is 200 and the width is 400;
    path.quadraticBezierTo(
        size.width * 0.75, size.height + 25, size.width, size.height * 0.75);

    path.lineTo(size.width, 0);
    path.close();

    return path;
  }

  @override
  bool shouldReclip(CustomClipper oldClipper) {
    return false;
  }
}

With the setup in place, let us proceed with adding validation to the authentication screen.

2. Adding TextFormField validation

To add validation, we will make changes to the AuthenticationTextFormField widget. This widget is used for creating the individual input text fields in our AuthenticationScreen widget.

authentication_text_form_field.dart

import 'package:flutter/material.dart';

class AuthenticationTextFormField extends StatelessWidget {
  const AuthenticationTextFormField({
    Key? key,
    this.confirmationController,
    required this.icon,
    required this.label,
    required this.textEditingController,
  }) : super(key: key);

  final TextEditingController? confirmationController;
  final IconData icon;
  final String label;
  final TextEditingController textEditingController;

  String? validate({required String? value}) {
    if (value!.isEmpty) {
      return 'This field cannot be empty.';
    }

    if ((key.toString().contains('email')) &&
        RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value) == false) {
      return 'This is not a valid email address.';
    }

    if ((key.toString().contains('password')) && value.length < 6) {
      return 'The password must be at least 6 characters.';
    }

    if ((key.toString().contains('password_confirmation')) &&
        value != confirmationController?.text) {
      return 'The password does not match.';
    }

    return null;
  }

  @override
  Widget build(BuildContext context) {
    return TextFormField(
      controller: textEditingController,
      obscureText: label.toLowerCase().contains('password'),
      decoration: InputDecoration(
        errorStyle: const TextStyle(fontSize: 14),
        floatingLabelStyle: const TextStyle(fontSize: 20),
        icon: Icon(
          icon,
          color: Theme.of(context).primaryColor,
        ),
        labelText: label,
      ),
      validator: (value) => validate(value: value),
    );
  }
}

To begin, we modified the AuthenticationTextFormField widget by adding the validate method. This method checks for various validation conditions based on the field's key and value. It checks:

  • if the field is empty;
  • if the email is in a valid format;
  • if the password length is sufficient;
  • and if the password confirmation matches the original password.

The validation method returns an error message if any of these conditions are not met.

In addition, within our constructor, we have included the confirmationController field. This field is responsible for verifying whether the "Password" field matches the "Password Confirmation" field.

Furthermore, we have made modifications to the TextFormField widget by adding two extra properties. The first property is placed within the InputDecoration widget and is used to increase the size of the error messages. The second property is the validator, which invokes our validate function.

Adjusting the AuthenticationScreen widget

Now that we finished the field validation we have to make sure that the validation is triggered when we submit our form. Flutter has built-in functionality to achieve this. We can use the GlobalKey that we assigned to our Form widget to access the state. In the state, we can check if the validation was successful. Let us take a look at the code to see how it works:

import 'package:flutter/material.dart';
import 'package:flutter_authentication_screen/widgets/authentication_text_form_field.dart';
import 'package:flutter_authentication_screen/widgets/wave.dart';

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

  @override
  State<AuthenticationScreen> createState() => _AuthenticationScreenState();
}

class _AuthenticationScreenState extends State<AuthenticationScreen> {
  final GlobalKey<FormState> _formKey = GlobalKey();
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  final passwordConfirmationController = TextEditingController();
  bool register = true;

  Future<void> _authenticate() async {
    if (_formKey.currentState!.validate() == false) {
      return;
    }

    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(
        content: Text('Success', textAlign: TextAlign.center, style: TextStyle(fontSize: 20),),
        backgroundColor: Colors.greenAccent,
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          const Wave(),
          Form(
            key: _formKey,
            child: Padding(
              padding: const EdgeInsets.only(left: 25.0, right: 25.0),
              child: Column(
                children: [
                  const SizedBox(height: 25),
                  AuthenticationTextFormField(
                    key: const Key('email'),
                    icon: Icons.email,
                    label: 'Email',
                    textEditingController: emailController,
                  ),
                  AuthenticationTextFormField(
                    key: const Key('password'),
                    icon: Icons.vpn_key,
                    label: 'Password',
                    textEditingController: passwordController,
                  ),
                  if (register == true)
                    AuthenticationTextFormField(
                      key: const Key('password_confirmation'),
                      confirmationController: passwordController,
                      icon: Icons.password,
                      label: 'Password Confirmation',
                      textEditingController: passwordConfirmationController,
                    ),
                  const SizedBox(height: 25),
                  ElevatedButton(
                    style: ElevatedButton.styleFrom(
                      minimumSize: const Size.fromHeight(50),
                      shape: const StadiumBorder(),
                    ),
                    onPressed: _authenticate,
                    child: Text(
                      register == true ? 'Register' : 'Login',
                      style: const TextStyle(fontSize: 17.5),
                    ),
                  ),
                  const SizedBox(height: 20),
                  InkWell(
                    onTap: () => setState(() {
                      register = !register;
                      _formKey.currentState?.reset();
                    }),
                    child: Text(
                      register == true ? 'Login instead' : 'Register instead',
                    ),
                  )
                ],
              ),
            ),
          )
        ],
      ),
    );
  }
}

In the above code snippet, we updated the AuthenticationScreen widget to trigger the form validation. We do this in our, _authenticate function that will be triggered when the form is submitted. As you can see we changed the onPressed property of our ElevatedButton to reference the function. Inside this function, we will check if the form validation is successful _formKey.currentState!.validate(). If it is, we will display a success message using a SnackBar otherwise the validation messages will be shown.

Because our validate function inside our AuthenticationTextFormField widget makes use of the key property we added them to every AuthenticationTextFormField instance with the proper String value. We also added the confirmationController property to our "Password Confirmation" field. This is necessary for our validate function to check if the "Password" field matches the password provided in the "Password Confirmation" field.

Now that we made the changes we can test out if our validation works:

flutter_validation_bottom_overflow_error

As you can see when we submit our form, we will get the validation messages, however as soon as we open our keyboard you will see that we have a bottom overflow error. We can easily fix that by using the SingleChildScrollView:

authentication_screen.dart

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: SingleChildScrollView(
      child: Column( ... ),
    ),
  );
}

To prevent the bottom overflow error when the keyboard is open, we wrapped the Column widget inside a SingleChildScrollView, making the screen scrollable when necessary.

flutter_validation_scroll_implementation

The GIF above shows that the problem has been fixed, and we no longer encounter any issues with the content overflowing at the bottom of the screen

After fixing the problem, we can continue testing the validation to make sure it works correctly.

flutter_validation_displaying_all_validation_messages

In the above GIF, we intentionally made mistakes to see if we get the correct error messages, and as you can see it works as we expected. The error messages popped up exactly as they should based on the rules we set for validation.

Now, let us move on to the last step and try submitting the form successfully.

flutter_validation_successfull_form_submission

Submitting the form with valid input also works as intended. Our validation does not prevent us from successfully submitting the form when we provide valid data.

Even though manual testing is very important I highly encourage you to write tests for all your validation functionality. If you need help, feel free to check out this post:

How to Create Unit Tests in Flutter using Dart
Unit testing is an essential part of the software development process that helps ensure the reliability and correctness of your code. In Flutter, unit testing plays a crucial role in verifying the functionality of individual units of code, such as functions or methods.

3. Change the Color of Validation Messages

At last, we can change the color of the validation messages. We can do this inside our main.dart file by adjusting the theme property:

theme: ThemeData(
  primaryColor: Colors.blueAccent,
  colorScheme: ThemeData().colorScheme.copyWith(
    primary: Colors.blueAccent,
    error: Colors.redAccent,
  ),
)

In the code snippet above, we simply added the error property and set it to Colors.redAccent. You are free to change it to any color you prefer.

flutter_validation_changed_colors_of_validation_messages

This small change slightly altered the color of our validation messages.

Conclusion

We have successfully added validation to our login screen in Flutter. Validation helps us make sure that the information entered by the user meets our requirements, which improves the security of the data and makes the user experience better. Throughout this post, we discussed the changes made to the AuthenticationTextFormField and AuthenticationScreen widgets to implement validation.

We also addressed the issue of bottom overflow by using the SingleChildScrollView widget. By following the steps outlined in this post, you can implement field validation in your authentication screens and create more robust Flutter applications.

Tags