Custom Validation Logic and Error Message Display in Flutter

In Flutter, validating user inputs is a crucial aspect of building robust and user-friendly applications. Custom validation logic allows you to enforce specific business rules and constraints on user data. Additionally, displaying informative error messages helps guide users in correcting their input, leading to a better user experience. This article delves into creating custom validation logic and effectively displaying error messages in Flutter forms.

Understanding Form Validation in Flutter

Flutter’s Form widget, combined with TextFormField, provides a straightforward way to manage and validate user input. Each TextFormField can have a validator property that takes a function responsible for validating the input. When the form’s validate() method is called, each validator is executed. If any validator returns a non-null string, it signifies an error, and the string is displayed as an error message.

Why Use Custom Validation Logic?

  • Business Rule Enforcement: Validate data according to specific business rules.
  • Data Integrity: Ensure data conforms to required formats and constraints.
  • User Guidance: Provide clear error messages that help users correct mistakes.

Implementing Custom Validation Logic

To implement custom validation logic, create a function that checks specific conditions and returns an error message (string) if the validation fails, or null if it passes.

Step 1: Set up a Basic Form

First, let’s create a simple form with a TextFormField for an email and a TextFormField for a password.

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Custom Validation Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: Text('Custom Validation Demo'),
        ),
        body: MyCustomForm(),
      ),
    );
  }
}

class MyCustomForm extends StatefulWidget {
  @override
  _MyCustomFormState createState() => _MyCustomFormState();
}

class _MyCustomFormState extends State {
  final _formKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextFormField(
              decoration: const InputDecoration(
                hintText: 'Enter your email',
                labelText: 'Email',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your email';
                }
                return null;
              },
            ),
            TextFormField(
              decoration: const InputDecoration(
                hintText: 'Enter your password',
                labelText: 'Password',
              ),
              validator: (value) {
                if (value == null || value.isEmpty) {
                  return 'Please enter your password';
                }
                return null;
              },
              obscureText: true,
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () {
                  if (_formKey.currentState!.validate()) {
                    ScaffoldMessenger.of(context).showSnackBar(
                      const SnackBar(content: Text('Processing Data')),
                    );
                  }
                },
                child: const Text('Submit'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Step 2: Implement Custom Validation Functions

Now, let’s add custom validation functions for email and password.

String? validateEmail(String? value) {
  if (value == null || value.isEmpty) {
    return 'Please enter your email';
  }
  String pattern = r'\w+@\w+\.\w+';
  RegExp regex = RegExp(pattern);
  if (!regex.hasMatch(value)) {
    return 'Please enter a valid email address';
  }
  return null;
}

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';
  }
  if (!value.contains(RegExp(r'[A-Z]'))) {
    return 'Password must contain at least one uppercase letter';
  }
  if (!value.contains(RegExp(r'[0-9]'))) {
    return 'Password must contain at least one digit';
  }
  return null;
}

Step 3: Integrate Custom Validators into the Form

Integrate these validation functions into your TextFormField widgets.

TextFormField(
  decoration: const InputDecoration(
    hintText: 'Enter your email',
    labelText: 'Email',
  ),
  validator: validateEmail,
),
TextFormField(
  decoration: const InputDecoration(
    hintText: 'Enter your password',
    labelText: 'Password',
  ),
  validator: validatePassword,
  obscureText: true,
),

Displaying Error Messages

Flutter automatically displays the error message returned by the validator in the TextFormField. However, customizing the error display can enhance the user experience.

Customizing Error Messages

You can customize the appearance of error messages using the InputDecoration class within the TextFormField. This includes changing the error style, error border, and error text style.

TextFormField(
  decoration: InputDecoration(
    hintText: 'Enter your email',
    labelText: 'Email',
    errorStyle: TextStyle(color: Colors.red),
    errorBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.red),
    ),
    focusedErrorBorder: OutlineInputBorder(
      borderSide: BorderSide(color: Colors.red),
    ),
  ),
  validator: validateEmail,
),

Handling Validation Logic Separately

For complex validation logic, consider moving the validation functions outside the TextFormField widget for better readability and maintainability.

class _MyCustomFormState extends State {
  final _formKey = GlobalKey();
  String? _email;
  String? _password;

  void _submitForm() {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();
      print('Email: $_email, Password: $_password');
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('Processing Data')),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Form(
        key: _formKey,
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            TextFormField(
              decoration: const InputDecoration(
                hintText: 'Enter your email',
                labelText: 'Email',
              ),
              validator: validateEmail,
              onSaved: (value) => _email = value,
            ),
            TextFormField(
              decoration: const InputDecoration(
                hintText: 'Enter your password',
                labelText: 'Password',
              ),
              validator: validatePassword,
              obscureText: true,
              onSaved: (value) => _password = value,
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: _submitForm,
                child: const Text('Submit'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

Advanced Validation Scenarios

Conditional Validation

Sometimes, validation depends on other form fields. Use the current state of the form to perform conditional validation.

TextFormField(
  decoration: const InputDecoration(
    hintText: 'Enter your confirm password',
    labelText: 'Confirm Password',
  ),
  validator: (value) {
    if (value != _password) {
      return 'Passwords do not match';
    }
    return null;
  },
  obscureText: true,
),

Real-Time Validation

For immediate feedback, validate the input as the user types using the onChanged property of the TextFormField.

TextFormField(
  decoration: const InputDecoration(
    hintText: 'Enter your email',
    labelText: 'Email',
  ),
  onChanged: (value) {
    _formKey.currentState?.validate();
  },
  validator: validateEmail,
),

Conclusion

Custom validation logic and effective error message display are essential for creating robust and user-friendly Flutter forms. By implementing custom validation functions and utilizing the InputDecoration class for customized error messages, you can guide users to input valid data and enhance the overall user experience. Incorporate these techniques into your Flutter applications to ensure data integrity and improve form usability.