Skip to content

fabrik_forms

pub.dev

fabrik_forms is a clean, testable, UI-agnostic form state management system for Flutter. It handles field values, validation, dirty/touched state, and reactive rebuilds — without coupling your logic to any specific widget.

Add the package to your pubspec.yaml:

dependencies:
fabrik_forms: ^0.1.0

Then run:

Terminal window
flutter pub get

Import it in your Dart files:

import 'package:fabrik_forms/fabrik_forms.dart';

Define your form with typed fields and validators:

final formNotifier = FabrikFormNotifier<String>(
FabrikForm({
'email': FabrikField(
value: '',
validators: [const EmailValidator()],
),
'password': FabrikField(
value: '',
validators: [
const PasswordValidator(
requireDigit: true,
requireSpecialChar: true,
),
],
),
}),
);

Wrap your UI with FabrikFormBuilder to rebuild reactively:

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

Every FabrikField tracks:

PropertyTypeDescription
valueTCurrent field value
errorString?Active validation error
visibleErrorString?Error shown only after the field is touched
isValidboolNo active errors
isTouchedboolUser has interacted with the field
isDirtyboolValue differs from initial

Call reset() to restore all fields to their initial values and clear all touched/dirty state:

formNotifier.reset();

This is useful for clearing a form after submission or on cancel.


const RequiredValidator()
const RequiredValidator(message: 'Name is required', trim: false)
const MinLengthValidator(min: 6)
const MaxLengthValidator(max: 100, message: 'Too long')
const EmailValidator() // required by default
const EmailValidator(isRequired: false) // optional — empty is valid
const EmailValidator(invalidMessage: 'Bad email')
const PasswordValidator() // requires 8+ chars, non-empty
const PasswordValidator(isRequired: false) // optional password
const PasswordValidator(
minLength: 12,
requireUppercase: true,
requireDigit: true,
requireSpecialChar: true,
)
const UrlValidator() // accepts http and https
const UrlValidator(requireHttps: true) // https only
const UrlValidator(isRequired: false) // optional — empty is valid
const PhoneValidator() // required by default
const PhoneValidator(isRequired: false) // optional — empty is valid
// Accepts: +1 234 567 8900 · (123) 456-7890 · 123-456-7890 · 1234567890
const RangeValidator(min: 1, max: 100)
const RangeValidator(min: 0.0, max: 1.0, minMessage: 'Too low', maxMessage: 'Too high')

Extend FabrikValidator<T> to create your own:

class UsernameValidator extends FabrikValidator<String> {
const UsernameValidator();
@override
String? call(String value) {
if (value.contains(' ')) return 'No spaces allowed';
return null;
}
}

  • Field-level validation with reusable, composable validators
  • Form-level state: .isValid, .isDirty, .isTouched, .values, .errors
  • markAllTouched() to reveal all errors on submit
  • reset() to restore initial state
  • ValueNotifier-based reactivity with FabrikFormNotifier
  • Declarative UI with FabrikFormBuilder
  • Works independently of the widget tree — fully unit-testable