Validators enforce field-level business rules in the domain. They are typically used in forms but represent domain knowledge (e.g., "what is a valid email address?" is a business rule, not a UI rule).
Validators use the FormField and FieldValidationFailure base classes (from the form_field package or an internal equivalent).
Validator classes must be named [FieldName]Field:
class EmailField { ... }
class PasswordField { ... }
class PhoneNumberField { ... }Failure classes must be named [FieldName]ValidationFailure:
class EmailValidationFailure { ... }
class PasswordValidationFailure { ... }Define a sealed class as the root for all failures of a given field, then enumerate specific failures as either enum values implementing the sealed class or concrete subclasses:
/// All validation failures for the password field.
sealed class PasswordValidationFailure extends FieldValidationFailure {
const PasswordValidationFailure();
}
/// Simple, enumerable password failures.
enum BasicPasswordValidationFailure implements PasswordValidationFailure {
empty,
noUppercase,
noDigit,
}
/// Failure with a dynamic minimum length.
class MinLengthPasswordValidationFailure extends PasswordValidationFailure {
final int minLength;
const MinLengthPasswordValidationFailure({required this.minLength});
}Validator classes must extend FormField<T, E extends FieldValidationFailure>:
class PasswordField extends FormField<String, PasswordValidationFailure> {
...
}Each validator must implement exactly two named constructors:
Represents the initial state of a field — before the user has interacted with it. Takes an initial value (required by default, may be optional if a sensible default exists).
const PasswordField.pure(super.value) : super.pure();
// With an optional initial value:
const PasswordField.pure([String value = '']) : super.pure(value);Represents the modified state — after the user has interacted with the field. Takes a required value and triggers validation.
const PasswordField.dirty(super.value) : super.dirty();Implement E? validator(T value). Return the appropriate failure object if the value is invalid, or null if it is valid. Validations run sequentially; return on the first failure.
@override
PasswordValidationFailure? validator(String value) {
if (value.isEmpty) {
return BasicPasswordValidationFailure.empty;
}
if (value.length < 8) {
return const MinLengthPasswordValidationFailure(minLength: 8);
}
if (!value.contains(RegExp(r'[A-Z]'))) {
return BasicPasswordValidationFailure.noUppercase;
}
if (!value.contains(RegExp(r'[0-9]'))) {
return BasicPasswordValidationFailure.noDigit;
}
return null;
}// domain/validators/email_field.dart
/// Validates an email address field.
class EmailField extends FormField<String, EmailValidationFailure> {
const EmailField.pure(super.value) : super.pure();
const EmailField.dirty(super.value) : super.dirty();
static final _emailRegExp = RegExp(
r'^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$',
);
@override
EmailValidationFailure? validator(String value) {
if (value.isEmpty) return .empty;
if (!_emailRegExp.hasMatch(value)) return .invalidFormat;
return null;
}
}
/// Validation failures for [EmailField].
sealed class EmailValidationFailure extends FieldValidationFailure {
const EmailValidationFailure();
}
enum BasicEmailValidationFailure implements EmailValidationFailure {
empty,
invalidFormat,
}class LoginState extends Equatable {
const LoginState({
this.email = const EmailField.pure(''),
this.password = const PasswordField.pure(''),
this.status = LoginStatus.initial,
});
final EmailField email;
final PasswordField password;
final LoginStatus status;
bool get isValid => email.isValid && password.isValid;
LoginState copyWith({ ... }) { ... }
@override
List<Object> get props => [email, password, status];
}