A Fluent Validation Library for Dart and Flutter

I've covered Flutter forms and validation before, but I always wanted to dig into the validation aspect of Flutter forms some more.  And, as I built out forms in Flutter that needed validation, I found myself thinking this can be done better.  This is what set me on the path to building "Flutter Validate".  This package is described as "A Flutter package for performing validation using a fluent interface approach.".   The package comes with a number of built in validators that will get you started on implementing the most common validation needs.  Plus, you can always extend it yourself or submit a pull request to the github repo if you would like to contribute.

Credit Given Where Credit Due

The Flutter Validate package is inspired largely by the great work of Jeremy Skinner who authored the .NET Fluent Validation library.  This library has been one of my main tools when building applications on the .NET platform.  So it just made sense to me to use an approach very similar to a great library that has been proven over time.  But to be clear my first version of Flutter Validate is not on par with the aforementioned tool.  But hopefully you find it useful as a package or as inspiration for you own work.  If the package gains momentum, I'll update it to include some of the missing features, such as localization/internationalization.

Are you Fluent in Fluent?

I chose to follow a Fluent interface design when building this package.  Why?  As mentioned earlier, I modeled my approach after the aforementioned  .NET Fluent Validation library. To be clear it is not meant to be a direct port but instead it merely draws inspiration from what I consider a great piece of work.  But I also think that the fluent interface suits validation very well.  If you disagree or dislike the fluent approach, then you can still use this library.  I'll cover this approach later but you really just need to use the various validators directly.  But I highly encourage trying out a fluent approach.  For example, here is how you might validate a phone number field on a form is entered correctly using a fluent approach:

contact.phone
  ..notEmpty()
  ..phoneNumber();

The key to a good fluent interface is it should be readable - think of it as your own domain specific language.  In this case the code clearly shows the phone number must not be empty and the text supplied must be a valid phone number.  If you are curious about the '..' placed in front of the notEmpty() and phoneNumber() calls, this is known as cascade notation in dart and it allows you to make a sequence of operations on the same object.  The above is technically equivalent to the code below, which is fine, but not fluent 😉

contact.phone.notEmpty();
contact.phone.phoneNumber();

I think a fluent approach can be appreciated even more as the complexity of the validation goes up.  Take this one for example:

contact.email
      ..notEmpty()
      ..emailAddress()
      ..must(beUniqueEmailAddress)
      ..when(() => contact.contactPreference == 'EMAIL');

The above code makes sure the email address is not empty, is a valid email address, is unique, and only applicable when the user has chosen email as their contact preference.  That's a pretty rich set of rules - and all are supported by Flutter Validate.

Note: the above blocks of code are pseudo-code meant to explain how a fluent approach looks.  Although the code examples are similar to how the Flutter Validate package works, I simplified it to keep the introduction simple.  But from here on out, we'll have nothing but working code examples...

Getting Started With Flutter Validate

Firstly, you want to get the package.  That is easy enough to do.  Add the following to your .yaml file.

dependencies:
  flutter_validate: ^1.0.0

Next, run the following command (sometimes this is not necessary as your editor may do this for you):

flutter packages get

Now that you have the package, lets get to validating.  The design of Flutter Validate (FV) is that you will derive your own validation class from AbstractValidator.

AbstractValidator

AbstractValidator is a generic class so you must declare it with the type of class you are going to validate.  For example you may want to validate a Contact class like this:

import 'package:flutter_validate/flutter_validate.dart';

class Contact {
  String name;
  String dob;
  String contactPreference = "EMAIL"; //PHONE, EMAIL
  String phone;
  String email;
  String password;
  String confirmPassword;
}

class ContactValidator extends AbstractValidator<Contact> {
  Contact contact;
  ContactValidator(this.contact) : super(contact);
}

We just created a simple Contact class that has fields common to capturing contact information.  We also created our validator class, extending AbstractValidator and making it use Contact as its generic type along the way.  That is all you need to do to start validating!

Let's take a peek inside of AbstractValidator.  There are currently four methods inside of here:

ruleFor(String keyFunction func→ RuleBuilder.  
Create a container to hold rules for a property. The value of the property should be returned by the passed in Function

validateRuleFor(String key→ ValidationResult.  
Validate all rules associated with the passed in key

validate(→ List<ValidationResult>
Validate all rules

errors(→ String
Returns all errors in a text format. Errors are separated by delimiter (default is a space). IMPORTANT: Executing this method has the side-effect of also executing validate().

You'll find that most often, you'll be using ruleFor and validateRuleFor.  Next, we will dig deeper into both...

Declaring Validation Rules

The way this library works is that you setup your validation rules up front.  The strength in this approach is your validation rules are not scattered about but rather in one place. This is exactly the purpose that the ruleFor() method serves. Take for example how we can setup validation for our Contact class:

//Validating an object always begins with creating a validator and is followed by 
    //adding rules to that validator.  In this case we created a ContactValidator by 
    //deriving from AbstractValidator<T>.  Then we begin adding rules to the validator
    //for each Contact property that needed validation.  This is the approach you would
    //take when validating business objects but again if you don't have an object, you
    //can use the underlying validators (e.g. notEmptyValidator) directly.
    myContactValidator = new ContactValidator(contact);
    myContactValidator.ruleFor("name", () => contact.name)
      ..notEmpty()
      ..withMessage('Name is required.')
      ..length(10, 20)
      ..withMessage('Name must be between 10 and 20 characters.');
    myContactValidator.ruleFor("dob", () => contact.dob)
      ..notEmpty()
      ..date();
    myContactValidator.ruleFor("phone", () => contact.phone)
      ..notEmpty()
      ..phoneNumber()
      ..when(() => contact.contactPreference == 'PHONE');
    myContactValidator.ruleFor("email", () => contact.email)
      ..notEmpty()
      ..emailAddress()
      ..must(beUniqueEmailAddress)
      ..when(() => contact.contactPreference == 'EMAIL');
    myContactValidator.ruleFor("password", () => contact.password)
      ..notEmpty()
      ..length(8, 16);
    myContactValidator.ruleFor("confirmPassword", () => contact.confirmPassword)
      ..equal(() => contact.password, 'Password');

Performing Validation

Similar to how we declare validation rules all in one place, you can and should also keep your validation in one place as well.  Here is an example that uses our contact validator.

//In Flutter, signaling a validation rule error is done by providing any
    //non-null text to a FormField's validator.  We declare this outside of the
    //build method just to keep our code easier to read.
    nameValidator =
        (value) => myContactValidator.validateRuleFor("name").errorText;
    dobValidator =
        (value) => myContactValidator.validateRuleFor("dob").errorText;
    phoneValidator =
        (value) => myContactValidator.validateRuleFor("phone").errorText;
    emailValidator = 
        (value) => myContactValidator.validateRuleFor("email").errorText;
    passwordValidator = 
        (value) => myContactValidator.validateRuleFor("password").errorText;
    confirmPasswordValidator = 
        (value) => myContactValidator.validateRuleFor("confirmPassword").errorText;

The job of the validateRuleFor() method is to locate the business rules you declared and validate them.  If any of the rules failed, errorText will contain a description of what failed validation.

Wire this up!

We have the two main chores behind us:

  1. Declare our validation rules
  2. Provide validation handlers

But we now have to wire this up to a flutter form.  Fortunately this is very simple for us to do but it does require two additional (albeit easy) tasks:

  1. Make sure as the form changes, those changes are immediately reflected in the underlying object attached to the form.  In this case that would be our Contact object.
  2. Tell our Flutter form about the validation handlers we have created.  Flutter requires that you provide a handler for any FormField that you wish to validate via its validator property.  This of course would be done in your build method inside of your Flutter app.

How to Update our Object When a FormField Changes

Flutter supports a pretty nifty way of doing this via the TextEditingController class.  There is a great example here, but the gist of it is simple and here is how you can accomplish it using our example contact form:


    final nameController = new TextEditingController();
    final dobController = new TextEditingController();
    final phoneController = new TextEditingController();
    final emailController = new TextEditingController();
    final passwordController = new TextEditingController();
    final confirmPasswordController = new TextEditingController();

    //Controllers are used to sync up the formfield data with the Contact view model.
    //This allows us to validate against the business object data.
    //We could just validate against the values coming from the form fields instead and that
    //might be the direction you go if you do not have a model or a really simple use case.
    nameController.addListener(() => contact.name = nameController.text);
    dobController.addListener(() => contact.dob = dobController.text);
    phoneController.addListener(() => contact.phone = phoneController.text);
    emailController.addListener(() => contact.email = emailController.text);
    passwordController.addListener(() => contact.password = passwordController.text);
    confirmPasswordController.addListener(() => contact.confirmPassword = confirmPasswordController.text);

And finally, you have to of course associate your controllers with the form fields.  Do this for each of your controllers and form fields:


          TextFormField(
            decoration: const InputDecoration(
              icon: const Icon(Icons.person),
              hintText: 'Enter your first and last name',
              labelText: 'Full Name',
            ),
            controller: nameController, //<-- assign controller here...
            validator: nameValidator,
          ),

How to Tell Flutter about Our Validation Handlers

Since we already declared our validation handlers, its only a matter of telling Flutter to use them.  This is done inside the build method where your form fields are declared.  Just assign the validator property of each form field to the appropriate validation handler.  For example:

          TextFormField(
            decoration: const InputDecoration(
              icon: const Icon(Icons.person),
              hintText: 'Enter your first and last name',
              labelText: 'Full Name',
            ),
            controller: nameController,
            validator: nameValidator, //<-- assign validation handler here...
          ),

Pretty simple stuff right?  This is all you need to get very well-organized and feature-rich validation in place for your Flutter forms.

Summary

Please check out the package and see what you think.  The package does have a fully functional Flutter app and unit tests that showcase the package.  Would love to hear your feedback or ideas for improvement. I am especially interested in support for additional validation scenarios/use cases that can be incorporated into the library.  Until next time!

1 thought on “A Fluent Validation Library for Dart and Flutter”

  1. AAAh this plugin is so damn goood. I give it 10 stars out of 5.
    Plugin sent from heaven.
    I tried implementing my own form validation but it really took a toll on my mind. Thanks for making it this easy to implement form validation.

    Be BLESSED Joe.

Leave a Reply

Your email address will not be published. Required fields are marked *