Step-by-Step Tutorial to Master Flutter TextField Validation

Flutter TextField Validation is the process of checking the input entered by a user in a text field to make sure that it meets some predefined criteria.

Step-by-Step Tutorial to Master Flutter Text Field Validation

The TextField widget is one of the fundamental UI components in Flutter, primarily used for capturing user text input. This widget is highly customizable, supporting various input types, styles, decorations, and interaction models. Developers can easily adjust its appearance and behavior to meet the specific needs of their applications. Let's dive into every aspects of Flutter TextField Validation, from basic to advanced techniques, for a better app.

>> Read more about Flutter coding:

What is TextField Validation?

TextField validation is the process of checking the input entered by a user in a text field to ensure it meets predefined criteria. These criteria can include format requirements, value ranges, required fields, and other rules that the input must satisfy before being accepted.

Importance of Flutter TextField Validation

User Experience

  • Guidance: Validation helps guide the users by providing immediate feedback on their input. This prevents users’ frustration and reduce user error and also makes the interaction smoother and more intuitive.
  • Clarity: By showing error messages and validating users’ input, users can easily understand what is expected for the input, which can enhance users’ overall experience and satisfaction with your app.
  • Prevention of errors: Validation prevents users from submitting forms with wrong or incomplete data, saving time and effort to correct them.

Data Integrity

  • Consistency: Makes sure the data entered meets specific criteria (type, format, length, content,…), maintains consistency of the app’s data sets.
  • Security: Proper data validation can prevent malicious input, such as SQL injection or cross-site scripting (XSS), therefore it can protect the app and its users.
  • Reliability: Ensures the data entered is reliable and trustworthy before storing and processing them.

Different Types of TextField Validation

Required Field Validation

Identifying mandatory fields in a form is crucial for both users and the application. It ensures that all necessary information is collected to complete a data set. Marking certain fields as required also enhances the user experience by reducing confusion and preventing input errors, thereby preventing incorrect data from being submitted to the system.

The TextFormField widget provides a validator property that can be used to define validation logic. Below is an example of how to use this property to check for empty input:

typescript
import 'package:flutter/material.dart';

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Enter your name',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter some text';
                }
                return null;
              },
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState?.validate() ?? false) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Processing Data')),
                  );
                }
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

In this example:

  • A Form widget is created with a GlobalKey for indentification.
  • The TextFormField is used to create an input field with a label.
  • The validator property of the TextFormField is set to a function that checks if the input is empty. If it is, an error message is returned.
  • An ElevatedButton is used to trigger form validation when pressed. If the form is valid, a snack bar message is displayed.

Data Format Validation

Proper data format validation ensures that user input matches expected patterns, improving data quality and preventing errors. Below are examples of common data formats and their validation techniques using Flutter.

  • Email Format Validation
typescript
String? validateEmail(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your email';
  }

  // Basic email regex pattern
  final RegExp emailRegex = RegExp(r'^[^@]+@[^@]+\.[^@]+');
  if (!emailRegex.hasMatch(value)) {
    return 'Please enter a valid email';
  }

  return null;
}
  • Username Validation
typescript
String? validateUsername(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your username';
  }

  // Username regex pattern (alphanumeric and underscores, 3-16 characters)
  final RegExp usernameRegex = RegExp(r'^[a-zA-Z0-9_]{3,16}$');
  if (!usernameRegex.hasMatch(value)) {
    return 'Username must be 3-16 characters long and contain only letters, numbers, and underscores';
  }

  return null;
}
  • Phone Number Validation
typescript
String? validatePhoneNumber(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your phone number';
  }

  // International phone number regex pattern
  final RegExp phoneRegex = RegExp(r'^\+?[1-9]\d{1,14}$');
  if (!phoneRegex.hasMatch(value)) {
    return 'Please enter a valid phone number';
  }

  return null;
}
  • URL Validation
typescript
String? validateURL(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter a URL';
  }

  // Basic URL regex pattern
  final RegExp urlRegex = RegExp(r'^(http|https):\/\/[^ "]+$');
  if (!urlRegex.hasMatch(value)) {
    return 'Please enter a valid URL';
  }

  return null;
}
  • Integrating These Validations into A Form
typescript
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Data Format Validation')),
        body: MyForm(),
      ),
    );
  }
}

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Email',
              ),
              validator: validateEmail,
            ),
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Username',
              ),
              validator: validateUsername,
            ),
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Phone Number',
              ),
              validator: validatePhoneNumber,
            ),
            TextFormField(
              decoration: InputDecoration(
                labelText: 'URL',
              ),
              validator: validateURL,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState?.validate() ?? false) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Processing Data')),
                  );
                }
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

Regular Expressions and Their Role in Advanced Format Validation

Regular expressions (regex) are sequences of characters that define a search pattern, primarily used for string matching and validation. They are powerful tools for enforcing complex validation rules on user input, ensuring the input meets specific format criteria.

Examples:

  • r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$': Validates email addresses.
  • r'^\(\d{3}\) \d{3}-\d{4}$': Validates US phone numbers in the format (123) 456-7890.

Content Validation

Content validation ensures that the data entered into a TextField meets specific requirements beyond just format. This includes constraints like minimum and maximum character length, ensuring only certain types of characters are entered, or more complex custom criteria.

  • Example 1: Minimum/Maximum Character Length for Passwords or Descriptions
typescript
String? validatePassword(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your password';
  }
  if (value.length < 8) {
    return 'Password must be at least 8 characters long';
  }
  if (value.length > 20) {
    return 'Password must be less than 20 characters long';
  }
  return null;
}

String? validateDescription(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter a description';
  }
  if (value.length < 10) {
    return 'Description must be at least 10 characters long';
  }
  if (value.length > 200) {
    return 'Description must be less than 200 characters long';
  }
  return null;
}
  • Example 2: Ensuring Only Alphabetic Characters are Entered for a Name Field
typescript
String? validateName(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your name';
  }

  final RegExp nameRegex = RegExp(r'^[a-zA-Z]+$');
  if (!nameRegex.hasMatch(value)) {
    return 'Name can only contain alphabetic characters';
  }

  return null;
}
  • Integrating These Validations into A Form
typescript
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Content Validation')),
        body: MyForm(),
      ),
    );
  }
}

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Password',
              ),
              obscureText: true,
              validator: validatePassword,
            ),
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Description',
              ),
              validator: validateDescription,
            ),
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Name',
              ),
              validator: validateName,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState?.validate() ?? false) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Processing Data')),
                  );
                }
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

Range Validation

Range validation ensures that numerical inputs provided by users fall within a specified minimum and maximum value. This is crucial in scenarios where numerical data must adhere to certain constraints, such as age limits, quantity restrictions, or other numerical boundaries.

Example: Checking Maximum and Minimum Values in Inputs

typescript
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: Text('Range Validation')),
        body: MyForm(),
      ),
    );
  }
}

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();
  final _ageController = TextEditingController();

  @override
  void dispose() {
    _ageController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              controller: _ageController,
              decoration: InputDecoration(
                labelText: 'Age',
              ),
              keyboardType: TextInputType.number,
              validator: validateAge,
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState?.validate() ?? false) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Processing Data')),
                  );
                }
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }

  String? validateAge(String? value) {
    if (value == null || value.isEmpty) {
      return 'Please enter your age';
    }

    final int? age = int.tryParse(value);
    if (age == null) {
      return 'Please enter a valid number';
    }

    if (age < 18) {
      return 'Age must be at least 18';
    }

    if (age > 65) {
      return 'Age must be less than or equal to 65';
    }

    return null;
  }
}

Explanation:

  • Age Field:
    • A TextFormField is created for age input, with the keyboardType set to TextInputType.number to facilitate numerical input.
    • The validateAge function checks the following:
      • If the input is empty, it returns an error message.
      • It attempts to parse the input as an integer. If parsing fails, it returns an error message indicating invalid input.
      • It checks if the parsed age is below the minimum allowed value (18) or above the maximum allowed value (65), returning appropriate error messages for each case.
  • Submit Button:
    • The ElevatedButton triggers the form validation when pressed. If the form is valid (i.e., all fields pass their validation checks), a snack bar with a processing message is displayed.

Potential Drawbacks and Mitigations of Flutter TextField Validation

While TextField validation is essential for ensuring data integrity and quality, it also has some potential drawbacks. Here are common issues and suggested mitigations:

Overly Complex Validation Can Hinder User Experience

Potential Drawbacks: Complex validation rules might confuse users, leading to frustration, and may cause them to abandon the form altogether.

Mitigation Strategies:

  • Simplify Validation Rules: Keep validation rules as straightforward and intuitive as possible.
  • Real-time Feedback: Provide real-time validation feedback as users fill out the form, rather than waiting until submission. This helps users correct mistakes immediately.
  • Clear Instructions: Provide clear instructions or placeholders within the form fields to guide users on the expected input format.
  • Visual Cues: Use visual cues like green checkmarks for valid inputs and red error messages for invalid ones.

Example:

typescript
TextFormField(
  decoration: InputDecoration(
    labelText: 'Password',
    hintText: 'Enter a password (8-20 characters)',
  ),
  validator: validatePassword,
)

Intimidating Error Messages Can Frustrate Users

Potential Drawbacks: Harsh or unclear error messages can confuse users, making them unsure of how to correct their mistakes.

Mitigation Strategies:

  • Friendly and Helpful Error Messages: Craft error messages that are friendly, specific, and provide guidance on how to correct the error.
  • Avoid Technical Jargon: Use simple, easy-to-understand language.

Example:

typescript
String? validatePassword(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your password';
  }
  if (value.length < 8) {
    return 'Password is too short. It should be at least 8 characters long.';
  }
  if (value.length > 20) {
    return 'Password is too long. It should be less than 20 characters long.';
  }
  return null;
}

Validation Can Introduce a Slight Delay in Form Submission

Potential Drawbacks: Validation, especially if complex or involving network requests, can introduce delays, making the form submission process seem slow.

Mitigation Strategies:

  • Client-side Validation: Perform as much validation as possible on the client side to reduce delays.
  • Optimized Code: Ensure that validation logic is optimized for performance.
  • Progress Indicators: Use progress indicators or loading spinners to provide visual feedback to users, letting them know that the form is processing.

Example:

typescript
ElevatedButton(
  onPressed: () {
    if (_formKey.currentState?.validate() ?? false) {
      // Show a loading indicator while processing
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('Processing Data...')),
      );
      // Simulate a delay or perform an async operation
      Future.delayed(Duration(seconds: 2), () {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('Data Submitted Successfully')),
        );
      });
    }
  },
  child: Text('Submit'),
)

Advanced Validation Techniques

Asynchronous Validation

Asynchronous validation is necessary when validation requires information from external sources, such as checking if a username is already taken. This type of validation often involves making API calls and can take some time, during which users should receive appropriate feedback.

Code Example:

typescript
class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();
  final _usernameController = TextEditingController();
  bool _isCheckingUsername = false;

  @override
  void dispose() {
    _usernameController.dispose();
    super.dispose();
  }

  Future<String?> _checkUsernameAvailability(String username) async {
    setState(() {
      _isCheckingUsername = true;
    });
    
    // Simulate network delay
    await Future.delayed(Duration(seconds: 2));
    
    setState(() {
      _isCheckingUsername = false;
    });
    
    // Simulated response
    if (username == "takenUsername") {
      return 'Username is already taken';
    }
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              controller: _usernameController,
              decoration: InputDecoration(
                labelText: 'Username',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter a username';
                }
                return null;
              },
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: _isCheckingUsername
                  ? null
                  : () async {
                      if (_formKey.currentState?.validate() ?? false) {
                        final errorMessage =
                            await _checkUsernameAvailability(
                                _usernameController.text);
                        if (errorMessage != null) {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text(errorMessage)),
                          );
                        } else {
                          ScaffoldMessenger.of(context).showSnackBar(
                            SnackBar(content: Text('Username is available')),
                          );
                        }
                      }
                    },
              child: _isCheckingUsername
                  ? CircularProgressIndicator(color: Colors.white)
                  : Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

Explanation:

  • The _checkUsernameAvailability function simulates an API call by introducing a delay using Future.delayed.
  • While waiting for the validation result, the _isCheckingUsername flag is used to show a loading indicator and disable the submit button.
  • The button’s onPressed callback performs the username check asynchronously and displays appropriate messages based on the validation result.

Combining Multiple Validation Rules

Multiple validation rules can be combined for a single text field to ensure the input meets all required criteria. This can be achieved using conditional statements.

Code Example:

typescript
String? validateEmail(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your email';
  }

  // Check email format
  final RegExp emailRegex = RegExp(
      r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$');
  if (!emailRegex.hasMatch(value)) {
    return 'Please enter a valid email';
  }

  // Check minimum length
  if (value.length < 5) {
    return 'Email must be at least 5 characters long';
  }

  return null;
}

Explanation:

  • The validateEmail function checks if the input is empty, matches the email format, and has a minimum length of 5 characters.
  • If any condition fails, an appropriate error message is returned.

Using Validation Packages

Validation packages in the Flutter ecosystem provide pre-built validators for common scenarios, reducing boilerplate code and simplifying validation implementation. Here are popular validation packages:

form_validator:

  • Pros:
    • Offers pre-built validators for common scenarios like email, URL, and phone numbers.
    • Simplifies the validation process with concise syntax.
  • Cons:
    • May introduce additional dependencies.
    • Might not cover all specific needs.
  • Code Example:
typescript
import 'package:flutter/material.dart';
import 'package:form_validator/form_validator.dart';

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Email',
              ),
              validator: ValidationBuilder().email().minLength(5).build(),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState?.validate() ?? false) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Processing Data')),
                  );
                }
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

validators:

  • Pros:
    • Provides a wide range of validators for various types of input.
    • Easy to use with simple function calls.
  • Cons:
    • Adds additional dependencies to the project.
    • Customization might be limited.
  • Code Example:
typescript
import 'package:flutter/material.dart';
import 'package:validators/validators.dart' as validator;

class MyForm extends StatefulWidget {
  @override
  _MyFormState createState() => _MyFormState();
}

class _MyFormState extends State<MyForm> {
  final _formKey = GlobalKey<FormState>();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          children: <Widget>[
            TextFormField(
              decoration: InputDecoration(
                labelText: 'Email',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your email';
                }
                if (!validator.isEmail(value)) {
                  return 'Please enter a valid email';
                }
                return null;
              },
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                if (_formKey.currentState?.validate() ?? false) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    SnackBar(content: Text('Processing Data')),
                  );
                }
              },
              child: Text('Submit'),
            ),
          ],
        ),
      ),
    );
  }
}

>> Read more: Making HTTP Requests in Flutter with Dio: A Step-by-Step Tutorial

Summary

Effective TextField validation in Flutter involves a combination of required field checks, format validation, content restrictions, and advanced techniques such as asynchronous validation and the use of validation packages. By implementing these best practices, you can ensure robust and user-friendly form validation, leading to higher data quality and an improved overall experience for your users.

>>> Follow and Contact Relia Software for more information!

  • Mobile App Development
  • coding
  • development