Flutter model form validation

Flutter model form validation provides many validators for Flutter, based on Dart plugin Respectable. This package replaces EasyValidation.

Get started

First, you need to add the following dependencies to your 'pubspec.yaml':

dependencies:
  reflectable: any
  build_runner: any
  flutter_model_form_validation: any

Complete list of available validators

ValidatorDescriptionProgress
ContainsDateTimeContainsDateTime validator permits you to check that a datetime value is into an array.done
ContainsNumberContainsNumber validator permits you to check that a number value is into an array.done
ContainsStringContainsString validator permits you to check that a string value is into an array.done
DateTimeRangeDateTimeRange validator permits you to check that a datetime using by declared settings.done
EmailEmail validator permits you to check that a string value is a valid email.done
EqualToDateTimeEqualToDateTime validator permits you to check that a value is equal to the value of another property.done
EqualToNumberEqualToNumber validator permits you to check that a value is equal to the value of another property.done
EqualToStringEqualToString validator permits you to check that a value is equal to the value of another property.done
FileMimeTypeFileMimeType validator permits you to check that a string value is a valid mime type.done
FileSizeFileSize validator permits you to check that a string value is a valid size.done
GreaterOrEqualToDateTimeGreaterOrEqualToDateTime validator permits you to check that a value is greater or equal to the value of another property.done
GreaterOrEqualToNumberGreaterOrEqualToNumber validator permits you to check that a value is greater or equal to the value of another property.done
GreaterOrEqualToStringGreaterOrEqualToString validator permits you to check that a value is greater or equal to the value of another property.done
GreaterThanDateTimeGreaterThanDateTime validator permits you to check that a value is greater than the value of another property.done
GreaterThanNumberGreaterThanNumber validator permits you to check that a value is greater than the value of another property.done
GreaterThanStringGreaterThanString validator permits you to check that a value is greater than the value of another property.done
ImageSizeImageSize validator permits you to check that a string value is a valid image size.done
InTextInText validator permits you to check that a string value is into a text.done
MembershipPasswordMembershipPassword validator permits you to check that a string value is accordance with declared settings.done
NotEqualToDateTimeNotEqualToDateTime validator permits you to check that a value is not equal to the value of another property.done
NotEqualToNumberNotEqualToNumber validator permits you to check that a value is not equal to the value of another property.done
NotEqualToStringNotEqualToString validator permits you to check that a value is not equal to the value of another property.done
NumberRangeNumberRange validator permits you to check that a number using by declared settings.done
PhoneNumberPhoneNumber validator permits you to check that a string value is a valid phone number.work in progress. I need documentation about phone number formats for each country in the world.
RegularExpressionRegularExpression validator permits you to check that a string value is accordance with provided regular expression.done
RequiredRequired validator permits you to check that a value is provided.done
SmallerOrEqualToDateTimeSmallerOrEqualToDateTime validator permits you to check that a value is smaller or equal to the value of another property.done
SmallerOrEqualToNumberSmallerOrEqualToNumber validator permits you to check that a value is smaller or equal to the value of another property.done
SmallerOrEqualToStringSmallerOrEqualToString validator permits you to check that a value is smaller or equal to the value of another property.done
SmallerThanDateTimeSmallerThanDateTime validator permits you to check that a value is smaller than the value of another property.done
SmallerThanNumberSmallerThanNumber validator permits you to check that a value is smaller than the value of another property.done
SmallerThanStringSmallerThanString validator permits you to check that a value is smaller than the value of another property.done
StringLengthStringLength validator permits you to check that a string value has a length between two values.done
StringRangeStringRange validator permits you to check that a string using by declared settings.done
URLURL validator permits you to check that a string value is a valid URL.done

How to use validators and validate a model

Here is a simple usage example. Note that a lot of examples are available in unit test file test/flutter_model_form_validation_test.dart.

How to define a class model with validation:

import 'package:flutter_model_form_validation/flutter_model_form_validation.dart';

@flutterModelFormValidator
class MyModel {
  MyModel(this.firstname, this.lastname, this.gender, this.birthday, this.dateOfDeath);
  
  @Required(error: 'Firstname is required')
  @StringLength(min: 3, max: 32, error: 'Firstname must have between 3 and 32 characters')
  final String firstname;
  
  @Required(error: 'Lastname is required')
  @StringLength(min: 3, max: 32, error: 'Lastname must have between 3 and 32 characters')
  final String lastname;

  @Required(error: 'Gender is required')
  @StringLength(min: 1, max: 1, error: 'Gender must have 1 character')
  final String gender;

  @Required(error: 'Birthday is requried')
  @DateTimeRange(min: '1900-01-01', max: null, error: 'Birthday must be betwwen 1900/01/01 and infinity')
  final DateTime birthday;

  @DateTimeRange(min: '1900-01-01', max: null, error: 'DateOfDeath must be betwwen 1900/01/01 and infinity')
  @GreaterOrEqualToDateTime(propertyName: 'birthday', error: 'DateOfDeath must be greater than Birthday')
  final DateTime dateOfDeath;
}

Each time you add a validator and each time you update your model class, you must regenerate a mapping file of your models and validators. If this file is not generated and update after any change, Flutter model form validation won't work !

Use this command line to get the file to get a new file named *.reflectable.dart into a flutter package project.

> flutter packages pub run build_runner build

Use this command line to get the file to get a new file named *.reflectable.dart into a flutter application project.

> flutter pub run build_runner build

How to validate a model:

  • Import the generated mapping file.
  • Call initializeReflectable funtion.
import 'package:flutter_model_form_validation/flutter_model_form_validation.dart';
import 'package:*.reflectable.dart';

initializeReflectable();

MyModel tester = new MyModel('Maxime', 'AUBRY', 'M', DateTime.parse('1986-12-22'), null);
bool isValid = ModelState.isValid<MyModel>(tester);

if (isValid) {
  // do stuff...
} else {
  print(ModelState.errors);
}

Practice usage for Flutter with Blocs:

Here is the login_form.dart file:

import 'package:example/blocs/login_form_bloc.dart';
import 'package:example/blocs/login_form_event.dart';
import 'package:example/blocs/login_form_state.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_model_form_validation/flutter_model_form_validation.dart';

class LoginForm extends StatelessWidget {
  final FocusNode _focusNode = FocusNode();

  @override
  Widget build(BuildContext context) {
    return BlocListener<LoginFormBloc, LoginFormState>(
      listener: (BuildContext context, LoginFormState state) {},
      child: Material(
        child: Form(
          child: Padding(
            padding: EdgeInsets.all(15),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: <Widget>[
                EmailInput(),
                PasswordInput(focusNode: this._focusNode),
                SubmitButton(),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class EmailInput extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginFormBloc, LoginFormState>(
      buildWhen: (LoginFormState previous, LoginFormState current) =>
          ModelFormUtilities.refreshWhen(previous, current),
      builder: (BuildContext context, LoginFormState state) {
        return TextFormField(
          decoration: InputDecoration(
            labelText: 'Email',
          ),
          autofocus: true,
          autovalidate: true,
          autocorrect: false,
          keyboardType: TextInputType.text,
          onChanged: (String value) =>
              context.bloc<LoginFormBloc>().add(LoginFormEmailChanged(value)),
          validator: (String value) =>
              ModelFormUtilities.getErrorMessage(state, 'email'),
        );
      },
    );
  }
}

class PasswordInput extends StatelessWidget {
  PasswordInput({Key key, @required this.focusNode}) : super(key: key);

  final FocusNode focusNode;
  final TextEditingController controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginFormBloc, LoginFormState>(
      buildWhen: (LoginFormState previous, LoginFormState current) =>
          ModelFormUtilities.refreshWhen(previous, current),
      builder: (BuildContext context, LoginFormState state) {
        return TextFormField(
          decoration: InputDecoration(
            labelText: 'Password',
            suffixIcon: IconButton(
              icon: Icon(
                Icons.info,
                color: Colors.grey,
              ),
              onPressed: () {
                alertDialogPasswordValidation(context, controller.text);
              },
            ),
          ),
          focusNode: this.focusNode,
          controller: controller,
          autovalidate: true,
          autocorrect: false,
          obscureText: true,
          keyboardType: TextInputType.visiblePassword,
          onChanged: (String value) => context
              .bloc<LoginFormBloc>()
              .add(LoginFormPasswordChanged(value)),
          validator: (String value) =>
              ModelFormUtilities.getErrorMessage(state, 'password'),
        );
      },
    );
  }
}

Future<void> alertDialogPasswordValidation(
    BuildContext context, String password) async {
  return showDialog<void>(
    context: context,
    barrierDismissible: false,
    builder: (BuildContext context) {
      Map<String, bool> rules = MembershipPassword.getErrorDetails(
          password, 8, 16, true, true, true, true);
      Map<String, String> labels = {
        'minLength': 'Min length of 8',
        'maxLength': 'Max length of 16',
        'includesAlphabeticalCharacters': 'At least one alphabetical character',
        'includesUppercaseCharacters': 'At least one uppercase character',
        'includesNumericalCharacters': 'At least one numeric character',
        'includesSpecialCharacters': 'At least one special character',
      };

      return AlertDialog(
        title: Text('Validation rules'),
        content: Container(
          width: double.maxFinite,
          child: ListView(children: <Widget>[
            for (String key in labels.keys)
              ListTile(
                leading: Icon(
                  rules[key] ? Icons.done : Icons.error,
                  color: rules[key] ? Colors.greenAccent : Colors.redAccent,
                ),
                title: Text(labels[key]),
              )
          ]),
        ),
        actions: <Widget>[
          FlatButton(
            child: Text('Ok'),
            onPressed: () {
              Navigator.of(context).pop();
            },
          ),
        ],
      );
    },
  );
}

class SubmitButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<LoginFormBloc, LoginFormState>(
      buildWhen: (LoginFormState previous, LoginFormState current) =>
          ModelFormUtilities.refreshWhen(
              previous, current, (p, c) => p.isValid != c.isValid),
      builder: (BuildContext context, LoginFormState state) {
        return RaisedButton(
          onPressed: (state.isValid ? () {} : null),
          child: Text('Submit form'),
        );
      },
    );
  }
}

Here is the authentication_model.dart file:

import 'package:dart_json_mapper/dart_json_mapper.dart' show jsonSerializable;
import 'package:flutter_model_form_validation/flutter_model_form_validation.dart';

@flutterModelFormValidator
@jsonSerializable
class AuthenticationModel {
  AuthenticationModel({
    this.email,
    this.password,
  });

  @Required(error: 'Email is required')
  @Email(error: 'Invalid email')
  String email;

  @Required(error: 'Password is required')
  @MembershipPassword(
      minLength: 8,
      maxLength: 16,
      includesAlphabeticalCharacters: true,
      includesUppercaseCharacters: true,
      includesNumericalCharacters: true,
      includesSpecialCharacters: true,
      error: 'Invalid password')
  String password;
}

Here is the authentication_form_bloc.dart:

import 'package:bloc/bloc.dart';
import 'package:example/blocs/login_form_event.dart';
import 'package:example/blocs/login_form_state.dart';

class LoginFormBloc extends Bloc<LoginFormEvent, LoginFormState> {
  LoginFormBloc() : super(const LoginFormState());

  @override
  Stream<LoginFormState> mapEventToState(LoginFormEvent event) async* {
    if (event is LoginFormEmailChanged)
      yield _mapUsernameChangedToState(event, state);
    else if (event is LoginFormPasswordChanged)
      yield _mapPasswordChangedToState(event, state);
  }

  LoginFormState _mapUsernameChangedToState(
    LoginFormEmailChanged event,
    LoginFormState state,
  ) {
    return state.updateUser(email: event.email);
  }

  LoginFormState _mapPasswordChangedToState(
    LoginFormPasswordChanged event,
    LoginFormState state,
  ) {
    return state.updateUser(password: event.password);
  }
}

Here is the authentication_form_event.dart file:

import 'package:equatable/equatable.dart';

abstract class LoginFormEvent extends Equatable {
  const LoginFormEvent();

  @override
  List<Object> get props => [];
}

class LoginFormEmailChanged extends LoginFormEvent {
  const LoginFormEmailChanged(this.email);

  final String email;

  @override
  List<Object> get props => [email];
}

class LoginFormPasswordChanged extends LoginFormEvent {
  const LoginFormPasswordChanged(this.password);

  final String password;

  @override
  List<Object> get props => [password];
}

Here is the authentication_form_state.dart file:

import 'package:example/entities/authentication_model.dart';
import 'package:example/main.reflectable.dart';
import 'package:flutter_model_form_validation/flutter_model_form_validation.dart';

class LoginFormState extends ModelFormState<AuthenticationModel> {
  const LoginFormState({
    AuthenticationModel viewmodel,
    bool isValid = false,
    Map<String, ValidationError> errors = const {},
  }) : super(viewmodel: viewmodel, isValid: isValid, errors: errors);

  LoginFormState updateUser({
    String email,
    String password,
  }) {
    initializeReflectable();
    AuthenticationModel copyOfViewmodel =
        ModelFormUtilities.getDeepCopy(this.viewmodel) ??
            new AuthenticationModel();
    copyOfViewmodel.email = email ?? this.viewmodel?.email ?? null;
    copyOfViewmodel.password = password ?? this.viewmodel?.password ?? null;

    return LoginFormState(
      viewmodel: copyOfViewmodel,
      isValid: ModelState.isValid<AuthenticationModel>(copyOfViewmodel),
      errors: ModelState.errors,
    );
  }
}

Feature requests and bug reports

Please file feature requests and bugs using the github issue tracker for this repository.

Libraries

flutter_model_form_validation