Not too long ago I published a Dart package to pub.dev called verify,

Its a package to do validations. In this post I want to overview some of its

capabilities and explain some approaches on how this could be used in a real

world application.

Verify overview

Verify defines a Validator as any function with the following signature:

This type of ideas arise naturally in function programming languages.



typedef Validator < S , T > = Either < List < ValidationError >, T > Function ( S subject );

As can be observed the Validator is generic over the input and output type of the function

and has a contract on what errors through an abstract class ValidationError.

For those who are familiar to these abstractions can already see how definitions of map, flatMap etc... can easily be derived.

Everything else boils down to creating different ways to combine these functions into more complex validators and having good ergonomics for developers to easily create custom validators.

Check the official API for more details.

Let build something

I won't get to theoretical with Verify instead jump straight into an example.

I will demonstrate how a sign up form page could be built and validated with

bloc and verify packages.

Are form will be real simple with an email, password and password confirmation input fields.

If you want to see the full source for this checkout out the examples

folder of the package repository.

Bloc setup

As you probably already know when building a reactive UI with Bloc, first thing is modeling state, and events.

For this we will use the freezed package to have value equality and some other goodies.

Events

Lets start out with modeling out events. We will need to be able to modify our states email, password and password confirmation fields. We usually would have submit event too. But lets keep it simple since we won't be connecting to service in this example.



@freezed abstract class SignUpEvent with _$SignUpEvent { const factory SignUpEvent . setEmail ( String email ) = SignUpSetEmail ; const factory SignUpEvent . setPassword ( String password ) = SignUpSetPassword ; const factory SignUpEvent . setPasswordConfirmation ( String password ) = SignUpSetPasswordConfirmation ; }

State

Lets start modeling our state. We now we have to track the email, password and password

confirmation field so we put all those in our state. We will also need to display validation errors this seems logical to track with a Map that contains errors grouped by

input field type, so that we can easily get the errors for an specific field.



enum SignUpFormField { email , password , passwordConfirmation , } @freezed abstract class SignUpState with _$SignUpState { const factory SignUpState ( { String email , String password , String confirmation , Map < SignUpFormField , List < String >> errors }) = _SignUpState ; factory SignUpState . initial () { return SignUpState ( errors: { SignUpFormField . email : [], SignUpFormField . password : [], SignUpFormField . passwordConfirmation : [], }); } }

the base bloc implementation should be pretty straight forward.



Stream < SignUpState > mapEventToState ( SignUpEvent event ) async * { final newState = event . when ( setEmail: ( str ) => state . copyWith ( email: str ), setPassword: ( str ) => state . copyWith ( password: str ), setPasswordConfirmation: ( str ) => state . copyWith ( confirmation: str ), ); yield newstate ; }

Integrate validations with Verify

To start out with verify, you first must define some error type and implement

ValidationError. To do this we will equip our error type with the field it references.



class SignUpError implements ValidationError { final SignUpFormField field ; final String message ; SignUpError ({ @required this . field , @required this . message }); @override String get errorDescription => message ; // ValidationError requirement }

Now that we have our error type in place. Let's define a validator.

I will show one way of doing this, but the same result could be achieved using the

provided verify combinators in different ways. Checkout out the docs to see

all available options.

Notice how we can bypass validators for some conditions. For instance we want to

have matching password and confirmation, but also want to ignore these checks if the confirmation field is null or perhaps empty.



final Validator_ < SignUpState > signUpValidation = Verify . all < SignUpState >([ Verify . subject < SignUpState >(). checkField ( ( state ) => state . password , Verify . property (( s ) => s . length > 4 , error: SignUpError ( field: SignUpFormField . password , message: 'incorrect length' , ))), Verify . subject < SignUpState >(). checkField ( ( state ) => state . email , Verify . property (( s ) => s . contains ( '@' ), error: SignUpError ( field: SignUpFormField . email , message: 'invalid format' , ))), Verify . property (( SignUpState state ) => state . confirmation == state . password , error: SignUpError ( field: SignUpFormField . passwordConfirmation , message: 'not match' , )). ignoreWhen (( state ) => state . confirmation == null ), ]);

Bloc integration

Finally lets update our bloc to validate the state after input updates:



Stream < SignUpState > mapEventToState ( SignUpEvent event ) async * { final newState = event . when ( setEmail: ( str ) => state . copyWith ( email: str ), setPassword: ( str ) => state . copyWith ( password: str ), setPasswordConfirmation: ( str ) => state . copyWith ( confirmation: str ), ); final errors = signUpValidation . verify < SignUpError >( newState ) . groupedErrorsBy (( error ) => error . field ) . messages ; yield newState . copyWith ( errors: errors ); }

The UI should be straightforward so I won't discuss that. If you want to see the complete example

head over to the examples folder of the repository.