fabrik_forms

A clean, UI-agnostic form state and validation system for Flutter.

pub.dev license platform


What's included

API What it does
FabrikField<T> Holds a value, validators, and metadata (isTouched, isDirty, error, visibleError)
FabrikForm Named field container with isValid, isDirty, values, errors, markAllTouched, reset
FabrikFormNotifier ValueNotifier wrapper for reactive form state
FabrikFormBuilder Declarative widget builder that rebuilds on form updates
RequiredValidator Ensures the field is not empty
EmailValidator Validates email format, optional or required
MinLengthValidator Enforces a minimum character count
MaxLengthValidator Enforces a maximum character count
PasswordValidator Configurable complexity rules (uppercase, digit, special char, min length)
UrlValidator Validates HTTP/HTTPS URLs, optional HTTPS-only mode
PhoneValidator Validates local and international phone number formats
RangeValidator Validates that a numeric value falls within an inclusive range
FieldsMatchValidator Form-level rule requiring two fields to be equal (password confirmation)
FabrikFormRule Form-level rule built from a plain function

Installation

dependencies:
  fabrik_forms: ^0.1.0
flutter pub get

Usage

Setting up a form

Each field declares its own type, so one form can mix String, int and bool values:

final formNotifier = FabrikFormNotifier(
  FabrikForm({
    'email': FabrikField<String>(
      value: '',
      validators: [const EmailValidator()],
    ),
    'password': FabrikField<String>(
      value: '',
      validators: [
        const PasswordValidator(
          requireDigit: true,
          requireSpecialChar: true,
        ),
      ],
    ),
    'age': FabrikField<int>(
      value: 18,
      validators: [const RangeValidator(min: 18, max: 120)],
    ),
    'subscribed': FabrikField<bool>(value: false),
  }),
);

Read fields back at their own type with get<T>:

final String email = formNotifier.get<String>('email').value;
final int age = formNotifier.get<int>('age').value;

Building the UI

FabrikFormBuilder(
  formNotifier: formNotifier,
  builder: (context, form, get) {
    final emailField = get<String>('email');
    final passwordField = get<String>('password');

    return Column(
      children: [
        TextField(
          onChanged: (val) => formNotifier.update('email', val),
          decoration: InputDecoration(
            labelText: 'Email',
            errorText: emailField.visibleError,
          ),
        ),
        TextField(
          onChanged: (val) => formNotifier.update('password', val),
          obscureText: true,
          decoration: InputDecoration(
            labelText: 'Password',
            errorText: passwordField.visibleError,
          ),
        ),
        ElevatedButton(
          onPressed: () {
            if (formNotifier.isValid) {
              // submit formNotifier.values
            } else {
              formNotifier.markAllTouched(); // reveal all errors
            }
          },
          child: const Text('Sign In'),
        ),
      ],
    );
  },
);

Resetting a form

// Restore all fields to initial values and clear touched/dirty state
formNotifier.reset();

Validators

RequiredValidator

RequiredValidator()
RequiredValidator(message: 'Name is required', trim: false)

EmailValidator

EmailValidator()                          // required by default
EmailValidator(isRequired: false)         // optional — empty is valid
EmailValidator(invalidMessage: 'Bad email')

MinLengthValidator / MaxLengthValidator

MinLengthValidator(min: 3)
MaxLengthValidator(max: 50, message: 'Keep it under 50 chars')

PasswordValidator

PasswordValidator()                          // requires 8+ chars, non-empty
PasswordValidator(isRequired: false)         // optional password
PasswordValidator(
  minLength: 12,
  requireUppercase: true,
  requireDigit: true,
  requireSpecialChar: true,
)

UrlValidator

UrlValidator()                          // accepts http and https
UrlValidator(requireHttps: true)        // https only
UrlValidator(isRequired: false)         // optional — empty is valid

PhoneValidator

PhoneValidator()                        // required by default
PhoneValidator(isRequired: false)       // optional — empty is valid
// Accepts: +1 234 567 8900 · (123) 456-7890 · 123-456-7890 · 1234567890

RangeValidator

RangeValidator(min: 1, max: 100)
RangeValidator(min: 0.0, max: 1.0, minMessage: 'Too low', maxMessage: 'Too high')

Custom validators

class UsernameValidator extends FabrikValidator<String> {
  const UsernameValidator();

  @override
  String? call(String value) {
    if (value.contains(' ')) return 'No spaces allowed';
    return null;
  }
}

Cross-field validation

Some rules span more than one field — password confirmation being the obvious one. Those live at the form level:

FabrikForm(
  {
    'password': FabrikField<String>(value: ''),
    'confirmPassword': FabrikField<String>(value: ''),
  },
  validators: [
    const FieldsMatchValidator(
      field: 'password',
      matchField: 'confirmPassword',
      message: 'Passwords do not match',
    ),
  ],
);

The result surfaces on the form rather than on a single field:

form.formError;   // 'Passwords do not match'
form.isValid;     // false — form-level rules count toward validity

For one-off rules, FabrikFormRule wraps a plain function:

FabrikFormRule(
  (values) => (values['end'] as int) > (values['start'] as int)
      ? null
      : 'End must be after start',
);

Field metadata

Property Type Description
value T Current field value
error String? First validation error (always set, regardless of touch)
errors List<String> Every failing rule, in validator order
visibleError String? First error, only exposed after the field is touched
visibleErrors List<String> Every error, only exposed after the field is touched
isValid bool No active errors
isTouched bool User has interacted with the field
isDirty bool Value differs from the original

Use errors when several rules should be shown at once:

Column(
  children: [
    for (final message in passwordField.visibleErrors) Text(message),
  ],
);

Documentation

Full API reference and guides at fabriktool.com


Contributing

Found a bug or have a suggestion? Open an issue or pull request on GitHub.

Maintainers

Libraries

fabrik_forms