How to add Validation to an Authentication Screen in Flutter
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.

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:

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:

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.

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.

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.

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:

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.

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.