formaestro 1.1.1
formaestro: ^1.1.1 copied to clipboard
Reactive, async-first form orchestration for Flutter with cross-field validation and delightful DX.
formaestro #
Reactive, async-first form orchestration for Flutter/Dart with cross-field rules, debounced async validators, and a tiny, UI-agnostic core built on SOLID + Clean Architecture.
- Predictable: immutable
FieldState, explicit streams, one orchestrator (Formaestro) - Async-friendly: debounce + async validators out of the box
- Scalable: cross-field rules that see the whole form
- UI-agnostic: plugs into any state management; optional
FieldXBuilderfor Flutter
Requirements: Dart SDK ≥ 3.3, Flutter ≥ 3.19.
Table of Contents #
- Install
- Quick Start
- Concepts
- Flutter Integration
- Cross-Field Rules
- API Reference (Cheat Sheet)
- Recipes
- Testing & Coverage
- Design & Architecture
- FAQ
- Contributing
- License
- Português (BR)
Install #
# pubspec.yaml
dependencies:
formaestro: ^1.1.0
// Your code
import 'package:formaestro/formaestro.dart';
Quick Start #
1) Validators and fields #
// Sync/async validators return a String (error message) or null.
String? required_(String s) => s.trim().isEmpty ? 'Required' : null;
Future<String?> uniqueUsername(String s) async {
await Future<void>.delayed(const Duration(milliseconds: 200));
return s == 'taken' ? 'Already taken' : null;
}
final username = FieldX<String>(
initialValue: '',
validators: [required_],
asyncValidators: [uniqueUsername],
debounce: const Duration(milliseconds: 300), // debounce for async validators
);
final password = FieldX<String>(
initialValue: '',
validators: [(v) => v.length < 8 ? 'Min 8 chars' : null],
);
final confirm = FieldX<String>(initialValue: '');
2) Compose a schema + cross-field rules #
final form = Formaestro(
FormaestroSchema({
'username': username,
'password': password,
'confirm' : confirm,
}, rules: [
// Fail if password != confirm
Rule.cross(['password', 'confirm'], (values) {
return values['password'] == values['confirm']
? null
: 'Passwords do not match';
}),
]),
);
3) Validate #
final ok = await form.validateAll();
if (!ok) {
// Read snapshots
final errors = form.errors; // Map<String, String?>
final values = form.values; // Map<String, dynamic>
}
Concepts #
FieldX<T> #
Holds a single field’s value, last error, and emits broadcast streams:
valueStream→TerrorStream→String?stateStream→FieldState<T>(value + error +isDirty+isValidating)
Behavior:
- Sync validators run immediately on
setValue(...). - Async validators run after debounce; first failing async validator wins.
isValidistrueiff there’s no error and not validating.- Call
dispose()to close streams and cancel timers (safe & idempotent).
field.setValue('new value'); // validate = true by default
field.setValue('draft', validate: false); // update without validating
Rule #
Cross-field rule that sees the entire form (Map<String, dynamic>):
Rule.cross(['a','b'], (values) => values['a'] == values['b'] ? null : 'a != b');
Rule.crossAsync(['email'], (values) async => await checkEmail(values['email']));
Formaestro #
The form orchestrator:
field<T>(key)→ typed access to aFieldX<T>values/errors→ snapshotsvalidateAll()→ triggers each field’s validation then applies cross-field rulesdispose()→ disposes all fields (and cancels debounced timers)
Flutter Integration #
The core is UI-agnostic. For a minimal Flutter integration, use the optional FieldXBuilder<T>:
FieldXBuilder<String>(
field: username,
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
onChanged: (v) => username.setValue(v),
decoration: InputDecoration(
labelText: 'Username',
errorText: state.error,
),
),
if (state.isValidating)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Text('Checking...', style: TextStyle(fontSize: 12)),
),
],
);
},
)
Prefer dumb widgets and drive everything via FieldX streams. Integrate with Bloc, Riverpod, ValueNotifier, etc.—FieldX is just streams + simple methods.
Cross-Field Rules #
Rules run after fields have validated:
final form = Formaestro(FormaestroSchema({
'pass': pass,
'confirm': confirm,
}, rules: [
Rule.cross(['pass','confirm'], (v) {
return v['pass'] == v['confirm'] ? null : 'Passwords do not match';
}),
Rule.crossAsync(['username'], (v) async {
// Optionally check server-side invariants here
return null;
}),
]));
validateAll()returnsfalseif any field fails or a rule returns a message.- Show cross-field errors wherever you want (a banner, a field helper, etc.).
API Reference (Cheat Sheet) #
// ---------- FieldState ----------
class FieldState<T> {
const FieldState({
required T value,
String? error,
bool isDirty = false,
bool isValidating = false,
});
T get value;
String? get error;
bool get isDirty;
bool get isValidating;
FieldState<T> copyWith({T? value, String? error, bool? isDirty, bool? isValidating});
}
// ---------- FieldX ----------
class FieldX<T> {
FieldX({
required T initialValue,
List<String? Function(T)> validators = const [],
List<Future<String?> Function(T)> asyncValidators = const [],
Duration? debounce,
});
// Snapshots
T get value;
String? get error;
bool get isValid;
// Streams (broadcast)
Stream<T> get valueStream;
Stream<String?> get errorStream;
Stream<FieldState<T>> get stateStream;
// Actions
void setValue(T value, {bool validate = true});
void dispose();
}
// ---------- Rule ----------
typedef Values = Map<String, dynamic>;
abstract interface class Rule {
List<String> get keys;
FutureOr<String?> call(Values values);
factory Rule.cross(List<String> keys, FutureOr<String?> Function(Values) fn);
factory Rule.crossAsync(List<String> keys, Future<String?> Function(Values) fn);
}
// ---------- Schema ----------
class FormaestroSchema {
const FormaestroSchema(Map<String, Object> fields, {List<Rule> rules = const []});
final Map<String, Object> fields; // expects FieldX at runtime
final List<Rule> rules;
}
// ---------- Orchestrator ----------
class Formaestro {
Formaestro(FormaestroSchema schema, {Duration debounce = const Duration(milliseconds: 300)});
FieldX<T> field<T>(String key);
Map<String, dynamic> get values;
Map<String, String?> get errors;
Future<bool> validateAll();
void dispose();
}
// ---------- Flutter adapter ----------
typedef FieldBuilder<T> = Widget Function(BuildContext, FieldState<T>);
class FieldXBuilder<T> extends StatefulWidget { /* ... */ }
Recipes #
-
Validate on submit (not on every keystroke)
onChanged: (v) => field.setValue(v, validate: false); // Later, e.g. onPressed: final ok = await form.validateAll(); -
Async username uniqueness
Future<String?> unique(String s) async => await api.exists(s) ? 'Already taken' : null; final username = FieldX<String>( initialValue: '', asyncValidators: [unique], debounce: const Duration(milliseconds: 250), ); -
Disable submit while validating
// state.isValidating is exposed on FieldState<T> inside FieldXBuilder final isBusy = passwordState.isValidating || usernameState.isValidating; // Disable button: onPressed: isBusy ? null : () async { ... } -
Map cross-field error to a specific field (UI choice)
final ok = await form.validateAll(); if (!ok) { final msg = /* evaluate rules again or surface a stored banner */; // show under 'confirm' field, for example }
Testing & Coverage #
Target ≥ 90% coverage.
flutter test --coverage
# Optional: HTML report (requires lcov/genhtml)
genhtml coverage/lcov.info -o coverage/html
open coverage/html/index.html
CI: see .github/workflows/ci.yml.
Design & Architecture #
Clean Architecture + SOLID separation:
lib/
src/
core/ # Debouncer and small primitives
domain/ # FieldX, FieldState, Rule, Formaestro, Schema
flutter_adapters/ # Optional: FieldXBuilder (thin UI adapter)
Principles:
- SRP:
FieldXmanages a single field;Formaestroorchestrates the form. - OCP: compose new validators/rules without modifying core types.
- DIP: domain is Flutter-free; adapters are thin and optional.
- Testability: deterministic streams and clear state snapshots.
Performance:
- Broadcast streams +
FieldStatesnapshots minimize rebuilds. - Debounced async validators reduce backend pressure.
Reliability:
dispose()cancels timers and ignores late emissions (safe & idempotent).
FAQ #
Is this a replacement for Form/TextFormField?
No—formaestro focuses on domain logic (validation, rules, state). Use any widgets you like.
Where should I show cross-field errors?
Anywhere—banner, under a specific field, or both. The library doesn’t enforce one style.
Can I use Bloc / Riverpod?
Yes. FieldX exposes streams; subscribe and map to your state management of choice.
How do I prevent validation on every keystroke?
Call setValue(value, validate: false) while typing and run validateAll() on submit.
Pub.dev polish checklist #
Add this to your pubspec.yaml for better pub.flutter-io.cn presence:
name: formaestro
description: Reactive, async-first form orchestration for Flutter/Dart with cross-field rules and debounced validators.
homepage: https://github.com/MarciohsjOliveira/formaestro
repository: https://github.com/MarciohsjOliveira/formaestro
issue_tracker: https://github.com/MarciohsjOliveira/formaestro/issues
documentation: https://pub.flutter-io.cn/documentation/formaestro/latest/
topics:
- forms
- validation
- reactive
- flutter
screenshots:
- description: "Hero"
path: assets/hero.png
- description: "Async validation with debounce"
path: assets/validators.png
Contributing #
PRs are welcome. Please:
- Run
flutter analyze - Keep/extend tests (goal ≥ 90% coverage)
- Update
CHANGELOG.mdwhen relevant - Keep public API docs in English
License #
MIT © 2025 MarciohsjOliveira — see LICENSE.
Português (BR) #
Leia a versão traduzida: README.pt-BR.md.