Design decisions
What the package deliberately is not, the residue that is recorded rather than fixed, the decisions that will not change, and how all of it is verified.
What it deliberately is not
advanced_forms does not… | …because |
|---|---|
| ship styled widgets | your design system already has them. A controller hands a widget exactly what it needs to bind to: a TextEditingController, a FocusNode, the typed state, the error. See Reusable field widgets |
wrap Flutter's Form | it is the validation system. No GlobalKey<FormState>, no autovalidateMode, and a TextFormField outside a Form is just a TextField with a decoration |
| format error messages | errors are values of your type E. A String is fine on day one; an enum or a sealed class scales to translations and to one field reporting several violations at once |
| use code generation | a build_runner step is a cost every consumer pays. copyWith is written by hand; the package has three small runtime dependencies |
depend on bloc, provider or riverpod | controllers are ChangeNotifiers. Own them in a StatefulWidget, a ChangeNotifierProvider, or whatever your app uses |
Deliberate residue
Recorded so it is not mistaken for oversight:
validmeans "no error recorded", not "checked and passed". A quiet form isvalid, socanSubmitistrueon it;await validate()is the guarantee. AFieldStatus.unverified— "never checked, or changed since" — would fix the confusion at the root and is the natural next state-model change, deferred because 0.2.0 was already a large break.- No predicate answers "not valid for any reason".
!isValidis also true forpending; writeisInvalidwhen you mean "has an error". - Illegal combinations are representable (
status: validbeside a non-null error) because the state is one class with a status enum. What prevents them in practice is that no caller ever picks a status.
Decisions that will not change
- No code generation, ever.
copyWithis hand-written.equatableis fine — a small, pure-Dart runtime dependency — and both state classes use it. AdvancedFieldStatestays one class with aFieldStatus, not a sealed hierarchy. Every status carries identical data, so there are no per-variant shapes to justify one.- Misuse throws.
addSubform,registerFields,setValidationEnabled,removeSubform,subscribeToFields,addRelationandsetValuethrow aStateErroron a disposed controller. A returned failure object that every caller ignores fails quietly; a throw fails loudly, and these guard programmer error. - The parent owns subform disposal.
removeSubformdetaches only. A subform has exactly one parent. - A re-entrant membership change double-wires children, and that is not fixed. A synchronous form listener that
calls
registerFields,addSubformorremoveSubformfrom inside those methods' own notification leaves children wired twice. The library never does this and it takes consumer code of a very specific shape; the fix exists and was measured, and was judged not worth its lines. - Line count is a feature. A controller over ~500 lines is treated as a defect and split into
partfiles; public dartdoc is written for a junior developer, not for completeness. Every feature is weighed against the lines it adds.
How this is verified
Every defect the 0.2.0 rewrite fixed was reproduced by execution before it was fixed, and every fix was checked by mutating the code to confirm the new test fails for the right reason. The suite is about 240 tests across 15 files. The gate table on the Validation modes page is pinned one cell per test; round ownership, the failure model, cross-field wiring, focus handling, coalescing and the text-controller sync each have a file.
The docs are verified the same way: every live example on this site is compiled into the Flutter bundle at build time, so a snippet the API has outgrown fails the build rather than misleading a reader.
validate() and failures
Verdict reuse, how validate() picks what to do, why concurrent calls coalesce, the failure model, and why form aggregates are derived on read.
FAQ and gotchas
The questions that come up in review, the type errors that look like package bugs and are not, and the behaviours that surprise on first contact.