Loading...
Loading...
Enforces Form + GlobalKey<FormState> with TextFormField whose sync validator returns a localized String? (never a hardcoded literal), AutovalidateMode.onUserInteraction, async availability checks moved OUT of the sync validator into a debounced Riverpod Notifier that surfaces errors through state, FocusNode/TextInputAction/onFieldSubmitted traversal, keyboardType/textCapitalization/autofillHints/TextInputFormatter, mandatory TextEditingController/FocusNode disposal, submit-enabled derived from validity (not stored), and scoped rebuilds so a keystroke never rebuilds the whole form. Use when building a Form, TextFormField, or FormField; wiring sync or async validation; managing FocusNode, focus traversal, autofocus, TextInputAction, onFieldSubmitted, or onEditingComplete; setting keyboardType, autofillHints, textCapitalization, or InputFormatter; disposing TextEditingController/FocusNode; enabling/disabling a submit button; or handling keyboard-avoidance on submit.
npx skill4agent add zakariaf/flutter-skills forms-and-inputFormStatereferences/validation-sync-and-async.mdvalidatorString?AutovalidateModereferences/focus-and-keyboard.mdFocusNodeautofocusFocusTraversalGroupTextInputActiononFieldSubmittedonEditingCompletekeyboardTypeautofillHintsTextInputFormatterscripts/check_forms.shTextEditingControllerFocusNodeStatedispose()state-management-riverpodvalidatorAppLocalizations.of(context).fieldRequired'Required'i18n-rtl-l10ncheck_forms.shvalidatorawaitFutureFormFieldValidator<T>String? Function(T?)dart:asyncTimerfakeAsyncAsyncValueInputDecoration.errorTextref.read(clockProvider).now()DateTime.now()service-boundary-and-nativeasync-safetybool_isValidFormStateflutter-performanceFormFieldsetStatewatchref.watch(....select(...))widget-compositionflutter-performanceAutovalidateModeAutovalidateMode.onUserInteractionalwayskeyboardTypetextCapitalizationautofillHintsTextInputFormatterautofillHintsInputDecoration.labelTexterrorTextaccessibility-as-codeFormGlobalKey<FormState>validate()save()class TaskForm extends StatefulWidget {
const TaskForm({super.key, required this.onSubmit});
final void Function(String title) onSubmit;
State<TaskForm> createState() => _TaskFormState();
}
class _TaskFormState extends State<TaskForm> {
final _formKey = GlobalKey<FormState>();
final _titleController = TextEditingController();
final _titleFocus = FocusNode();
void dispose() {
_titleController.dispose(); // rule 1: always dispose
_titleFocus.dispose();
super.dispose();
}
void _submit() {
if (_formKey.currentState?.validate() ?? false) {
widget.onSubmit(_titleController.text.trim());
}
}
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
return Form(
key: _formKey,
autovalidateMode: AutovalidateMode.onUserInteraction, // rule 7
child: Column(
children: [
TextFormField(
controller: _titleController,
focusNode: _titleFocus,
autofocus: true,
textInputAction: TextInputAction.done,
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(labelText: l10n.taskTitleLabel),
validator: (value) => // rule 2 + 3: localized, pure
(value == null || value.trim().isEmpty) ? l10n.fieldRequired : null,
onFieldSubmitted: (_) => _submit(),
),
],
),
);
}
}validatornullString? validateTitle(String? value, AppLocalizations l10n) {
final text = value?.trim() ?? '';
if (text.isEmpty) return l10n.fieldRequired;
if (text.length > 120) return l10n.fieldTooLong; // structural bound, not design
return null;
}clockProvidererrorTextvalidatorreferences/validation-sync-and-async.mdexamples/async_field_notifier.dart// The field is driven by Notifier state, not by an async validator.
final status = ref.watch(nameAvailabilityNotifierProvider);
TextFormField(
controller: _nameController,
onChanged: ref.read(nameAvailabilityNotifierProvider.notifier).onNameChanged,
decoration: InputDecoration(
labelText: l10n.accountNameLabel,
errorText: switch (status) {
AsyncData(:final value) when value == NameCheck.taken => l10n.nameTaken,
AsyncError() => l10n.nameCheckFailed,
_ => null, // idle / loading / available: no error
},
),
);TextInputAction.next.doneonFieldSubmittedFocusScope.of(context).nextFocus()FocusTraversalGroupreferences/focus-and-keyboard.mdTextFormField(
focusNode: _titleFocus,
textInputAction: TextInputAction.next,
onFieldSubmitted: (_) => _dueDateFocus.requestFocus(),
),_isFormValidfinal submitting = ref.watch(taskFormNotifierProvider).isLoading;
FilledButton(
onPressed: submitting ? null : _submit, // rule 5
child: Text(l10n.saveAction),
);SingleChildScrollViewScaffoldresizeToAvoidBottomInset: truebottomNavigationBarvalidator: (v) async => await repo.isTaken(v)Future'Required''Invalid email'AppLocalizationsTextEditingControllerFocusNodebuild()Statedispose()bool _isValidonChangedautovalidateMode: AutovalidateMode.alwaysTextEditingControllersetStatedebounceDateTime.now()dart:asyncTimerfakeAsyncref.read(clockProvider).now()DateTime.now()FocusNodecheck_forms.shvalidatorAppLocalizationsawaitFuturevalidatorerrorTextAutovalidateMode.onUserInteractionkeyboardTypetextInputActionautofillHintsInputDecorationstate-management-riverpodasync-safetyi18n-rtl-l10nAppLocalizations.of(context)accessibility-as-codeflutter-performancewidget-composition_buildFieldnavigation-and-routingPopScopeservice-boundary-and-nativeclockProvider