Skip to main content

advanced-forms-build-forms

Build Flutter forms with the advanced_forms package (AdvancedFormController, AdvancedTextFieldController, AdvancedFieldBuilder). Use whenever the user creates a form, adds or edits form fields, wires validation (sync, async, or cross-field), chooses when errors appear (ValidationMode), builds dropdowns/checkboxes/multi-selects/sliders bound to field controllers, handles submit buttons or server-side errors, or works with subforms in any Flutter project — whether or not it depends on advanced_forms yet (add it with `flutter pub add advanced_forms`) — even if they never say the word "form".

ソース情報

リポジトリ
leancodepl/advanced_forms
ソースの最終更新活動
2026年9月23日 12:11
検出された SKILL.md の言語
英語
スター
19
フォーク
2

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
advanced_forms-build-forms
description
Build Flutter forms with the advanced_forms package (AdvancedFormController, AdvancedTextFieldController, AdvancedFieldBuilder). Use whenever the user creates a form, adds or edits form fields, wires validation (sync, async, or cross-field), chooses when errors appear (ValidationMode), builds dropdowns/checkboxes/multi-selects/sliders bound to field controllers, handles submit buttons or server-side errors, or works with subforms in any Flutter project — whether or not it depends on advanced_forms yet (add it with `flutter pub add advanced_forms`) — even if they never say the word "form".
# advanced_forms Typed form controllers for Flutter, no framework on top. Everything is `ChangeNotifier` + `ValueListenable` from the SDK. One import: `package:advanced_forms/advanced_forms.dart`. Requires Flutter >= 3.19.0 (Dart 3.3). A **form controller** (`AdvancedFormController`) owns **field controllers** (`AdvancedFieldController<T, E>`). `T` is the value type, `E` is *your* error type (`E extends Object`, never nullable — `null` means "no error"). Plain `String` errors are fine; an enum or sealed class scales better. Widgets subscribe to one field each via `AdvancedFieldBuilder`; the form aggregates tree-wide state. Snippets assume in scope: `MyError`, your error enum; `form`, a form controller; `translate`, an `ErrorTranslator<MyError>`. ## Setup Before writing form code, check that `pubspec.yaml` lists `advanced_forms` under `dependencies`. If it does not, add it from the project root — do not hand-write a version: ```sh flutter pub add advanced_forms ``` That pins the current release and runs `pub get`. Then import `package:advanced_forms/advanced_forms.dart` and continue below. If the project has `leancode_forms` instead, it is the discontinued predecessor: the API in this skill replaces it (`AdvancedFormController` for `FormGroupCubit`, `Advanced*Controller`s for the `*Cubit`s), see the migration guide in the repository before porting. ## Quick start ```dart import 'package:advanced_forms/advanced_forms.dart'; enum SignupError { required, tooShort } class SignupFormController extends AdvancedFormController { SignupFormController() { registerFields([email, password]); } final email = AdvancedTextFieldController( validator: filled(SignupError.required), ); final password = AdvancedTextFieldController( validator: filled(SignupError.required) & atLeastLength(8, SignupError.tooShort), ); Future<bool> submit() async { if (!await validate()) { return false; } // values were checked — safe to send: email.fieldValue, password.fieldValue return true; } } ``` ```dart // Inside SignupScreen.build: final form = context.read<SignupFormController>(); return Column( children: [ _SignupField(field: form.email, label: 'Email'), _SignupField(field: form.password, label: 'Password'), ElevatedButton( // A block body that awaits. `onPressed: form.submit` compiles but drops the // future, losing the result and turning any throw into an unhandled error. onPressed: () async { await form.submit(); }, child: const Text('Sign up'), ), ], ); ``` ```dart // One reusable field widget, at top level. class _SignupField extends StatelessWidget { const _SignupField({required this.field, required this.label}); final AdvancedTextFieldController<SignupError> field; final String label; @override Widget build(BuildContext context) { return AdvancedFieldBuilder<String, SignupError>( field: field, builder: (context, state, _) => TextFormField( controller: field.textController, // NEVER your own TextEditingController focusNode: field.focusNode, readOnly: state.readOnly, // add enabled: !state.readOnly to also look disabled decoration: InputDecoration( labelText: label, errorText: state.error == null ? null : translate(state.error!), ), ), ); } } ``` Default UX: **nothing validates until `submit`.** Errors appear on the first `validate()`, and editing a field clears its error — but the error only returns on the next `validate()`. For live feedback, set a `ValidationMode`; see *Validation*. Two rules prevent most bugs: - **Call `registerFields()` once, in the constructor, with every field.** The parameter is `List<AdvancedFieldController<dynamic, dynamic>>`, so mixed value types go in one list — let the literal infer its type rather than annotating it. The form then owns their lifecycle — disposal, `wasModified` tracking, `validate()`, `resetAll()`. A second call *replaces* the list: earlier fields stop participating but are still disposed with the form. (Deliberately re-registering the *same* list to re-baseline an edit form is the one exception — see *Prefilling an edit form*.) A field never registered at all belongs to nobody and is never disposed. - **Bind text widgets to `field.textController`.** The field owns a `TextEditingController` and keeps it in two-way sync, so do not allocate your own and do not wire `onChanged` + `initialValue` for text fields. ## Field controllers | Controller | Value type | Constructor | | --- | --- | --- | | `AdvancedTextFieldController<E>` | `String` | `{initialValue = '', validator, asyncValidation, focusNode, name}`; owns `textController` | | `AdvancedBooleanFieldController<E>` | `bool` | `{initialValue = false, validator, asyncValidation, focusNode, name}` | | `AdvancedSingleSelectFieldController<V, E>` | `V?` | `{required V? initialValue, required List<V> options, validator, asyncValidation, focusNode, name}`; set with `select(V?)`, `null` clears; `setOptions(List<V>)` swaps the list | | `AdvancedMultiSelectFieldController<V, E>` | `Set<V>` | `{required Set<V> initialValue, required List<V> options, validator, asyncValidation, focusNode, name}`; set with `toggleElement` / `addValue` / `removeValue`; `setOptions(List<V>)` swaps the list | All support `reset()`, `prefill()`, `markReadOnly()` / `unmarkReadOnly()`, `setError()`, `clearErrors()`, `setValidationMode()`, `validate()`, `subscribeToFields()`, `revalidateSync()`, `focus()`, `handleUnfocus()`, `getValueSetter()` (a `ValueSetter<T>?`, `null` while read-only, so a widget with a nullable `onChanged` disables itself), and the getters `fieldValue`, `error`, `name`, `lastFailure`, `isDisposed`. - **`focusNode` is on every controller**, not just the text one — the field makes and owns it; pass `focusNode:` to bind one you own and the field never disposes it. `focus()` only moves focus if **some widget actually binds `field.focusNode`**: a `TextField`, or a `Focus` / `FocusableActionDetector` around a slider, chip group or dropdown. A field nobody bound is a silent no-op — that is why "focus the first invalid field" seems to do nothing on non-text fields. - `name` is a readable `String?`. It labels the field in package error reports and as the `FocusNode` debug label, and is the natural key for error summaries and server-error maps. - `V` is unbounded on both selects. `options` is a **non-nullable** `List<V>`; only `initialValue` is nullable, on the single select. A required dropdown is `initialValue: null` plus `validator: notNull(MyError.required)`. - `select` and `addValue` **assert** the argument is one of `options` — and so does `toggleElement` when it adds. `prefill` does not assert, so check server-supplied values. - Both selects copy the `options` (and the multi-select the set) you pass, so mutating them later never reaches the field. `const {}` is a **Map**: an empty initial selection is `const <Topping>{}`. - **Options that depend on another field or on a request:** `field.setOptions(newList)`, e.g. from `aircraft.addListener(() => instructor.setOptions(instructorsFor(aircraft.fieldValue)))`. A selected value not on the new list is cleared (dropped, on the multi-select) as a *program* write, like `prefill`: an untouched field stays untouched, an edited one shows its `notNull` error at once; read-only fields are cleared too. Widgets rebuild even if the value stayed. Membership is `==` — a server copy of an option is not the option on the list, so use value types or resolve by id. - With no `validator`, `E` infers to its bound `Object`. That compiles and then breaks every `switch` on your error enum, so spell it out: `AdvancedTextFieldController<MyError>()`. - `AdvancedFieldController<T, E>` is **concrete** — construct it directly for any value with no controller of its own (a slider, stepper, rating, derived total): `AdvancedFieldController<int, MyError>(initialValue: 0, name: 'total')`. Full constructor: `{required T initialValue, Validator<T, E>? validator, AsyncValidation<T, E>? asyncValidation, FocusNode? focusNode, String? name}`. Ordinary writes need no `force:` — that is only for writing to a field you marked read-only. To transform text as the user types (normalize, mask, uppercase), extend `AdvancedTextFieldController<E>` and override `setValue`: ```dart class PhoneFieldController<E extends Object> extends AdvancedTextFieldController<E> { PhoneFieldController({ String initialValue = '', super.validator, super.asyncValidation, super.focusNode, super.name, }) : super(initialValue: _digits(initialValue)); static String _digits(String value) => value.replaceAll(RegExp(r'\D'), ''); @override void setValue(String newValue, {bool force = false}) => super.setValue(_digits(newValue), force: force); } ``` Every write to `textController` — keystroke, paste, programmatic `.text =` — goes through the public `setValue`, so the override always runs and the transformed value is written back with the caret kept on the same characters. Three writes **bypass** `setValue` and need the same treatment: the constructor's `initialValue` (done above), `reset()` (returns to that same normalized value), and `prefill()`. Everything you do not transform forwards as a super-parameter; `initialValue` cannot, because it has to pass through `_digits` first, so it goes in the explicit `super(...)`. The two mix freely. Signatures for overriding: `void setValue(T newValue, {bool force = false})`, `void prefill(T newValue, {bool force = false})`. ### Field state — `AdvancedFieldState<T, E>` `AdvancedFieldBuilder` hands you this; `field.value` **is** this state object. State is value-equal, so a no-op write notifies nobody. | Member | Meaning | | --- | --- | | `value` | the current value — `field.fieldValue` is the shortcut | | `error` | `validationError ?? asyncError` — `field.error` is the shortcut | | `validationError` / `asyncError` | what the sync path (validator or `setError`) / the async round recorded | | `status` | `FieldStatus.valid` \| `invalid` \| `pending` \| `validating` \| `failedValidation` | | `isValid` / `isInvalid` | status is `valid` / `invalid` | | `isPending` / `isValidating` / `isInProgress` | waiting out the debounce / request in flight / either | | `isFailedValidation` | the async validator threw or timed out | | `readOnly` | `setValue` is a no-op (pass `force: true` to override) | | `validationMode` | the **effective** mode — a field whose form has validation switched off reports `manual` | `valid` means *no error recorded*, not *checked and passed*: a field nobody validated yet is `valid`. There is **no** "has this been checked" or "was this submitted" flag — `await validate()` is the guarantee; keep your own bool if the UI needs one. ## Binding widgets `AdvancedFieldBuilder<T, E>` wraps `ValueListenableBuilder` and rebuilds only when its own field notifies, so a keystroke rebuilds one subtree. Its third builder parameter is a `child:` passthrough for a subtree that does not depend on field state. Type arguments match the controller: `<String, E>` text, `<bool, E>` boolean, `<V?, E>` single select (over an `AdvancedSingleSelectFieldController<V, E>`), `<Set<V>, E>` multi select. Disable the widget while `state.readOnly`: pass `getValueSetter()` wherever the callback is a `ValueSetter<T>?` (`Switch.onChanged` takes it directly), and `state.readOnly ? null : …` where it is not. ```dart // Dropdown — DropdownButton in an InputDecorator compiles on every supported SDK. // Prefer it over DropdownButtonFormField, whose value parameter was renamed across versions. AdvancedFieldBuilder<Country?, MyError>( field: form.country, builder: (context, state, _) { final error = state.error; // promote E? to E: ErrorTranslator takes a non-null E return InputDecorator( decoration: InputDecoration(errorText: error == null ? null : translate(error)), child: DropdownButton<Country>( value: state.value, isExpanded: true, items: [ for (final option in form.country.options) DropdownMenuItem(value: option, child: Text(option.name)), ], onChanged: state.readOnly ? null : form.country.select, ), ); }, ) // A scalar field, wrapped in Focus so focus() can reach it. AdvancedFieldBuilder<int, MyError>( field: form.rating, builder: (context, state, _) { final setter = form.rating.getValueSetter(); return Focus( focusNode: form.rating.focusNode, // without this, form.rating.focus() does nothing child: InputDecorator( decoration: InputDecoration( errorText: state.error == null ? null : translate(state.error!), ), child: Slider( value: state.value.toDouble(), min: 0, // Slider asserts min <= value <= max — cover the field's initial value max: 5, divisions: 5, onChanged: setter == null ? null : (v) => setter(v.round()), ), ), ); }, ) ``` A `Focus` wrapper makes the node reachable so `focus()` lands there; whether the widget inside paints a focus ring is its own business. Widgets with no `errorText` slot — `Switch`, `CheckboxListTile`, a `Wrap` of `FilterChip`s for a multi-select — render `state.error` yourself or wrap them in an `InputDecorator` as above. Adapt the setter where the signature differs: `CheckboxListTile.onChanged` is `ValueChanged<bool?>?`, so `onChanged: setter == null ? null : (v) => setter(v ?? false)`. `ErrorTranslator<E>` is `String Function(E)` — a plain function, so write it wherever the labels belong: `String translate(MyError e) => switch (e) { MyError.required => 'Required', … };`. It takes a **non-null** `E` while `state.error` is `E?`, so promote before calling. One shared translator per enum is the cheapest start; pass a field-specific one where the wording differs ("Required" vs "Pick a country"). In a real app wrap each binding once in a reusable widget taking `field` + `ErrorTranslator<E>`. Do not wrap fields in Flutter's `Form` — no `Form`, no `GlobalKey<FormState>`, no `autovalidateMode`. This package *is* the validation system and a second one fights it. (A `TextFormField` outside a `Form` is fine — unregistered, it is a `TextField` with a decoration.) ## Validation ```dart
GitHubで見る
この SKILL.md は非常に大きいため、SkillsMP では最初のセクションだけを表示しています。 GitHubで見る