flutterx_forms
Typed Flutter form fields with validators and one form controller: text, integer and double with arrows, checkbox and switch, dropdowns, date and time, file and files with drop on the web, lists of fields, tabs, chips and dynamic inner forms.
Installation
flutter pub add flutterx_forms
import 'package:flutterx_forms/flutterx_forms.dart';
Setup
Mix FormController into the State of the form. Every field has a handle, XFormField<T>, created with field()
before the form is built: it gives the value, the focus node and the controller. The mixin builds the Form, disposes
the handles with the state and calls onFormSubmit when the form is valid.
class _LoginState extends State<Login> with FormController<Login> {
late final XFormField<String> _email = field();
late final XFormField<String> _password = field();
@override
Widget build(BuildContext context) => buildForm(
autovalidateMode: AutovalidateMode.onUserInteraction,
children: [
XTextFormField(
field: _email,
decoration: const InputDecoration(labelText: 'Email'),
textInputAction: TextInputAction.next,
validator: validateFirst([nonBlank(() => 'Required'), matchesEmail(() => 'Not an email')]),
),
XTextFormField(field: _password, obscureText: true, validator: minLen(8, () => 'At least 8 characters')),
FilledButton(onPressed: submitForm, child: const Text('Sign in')),
],
);
@override
Future<void> onFormSubmit() => signIn(_email.value, _password.value);
}
submitForm() validates, unfocuses and runs onFormSubmit; validateForm(), resetForm() and saveForm() act on
every field. formFields lists the attached handles; with autoscroll overridden to true a failed submit focuses the
first invalid field and scrolls to it. A handle read before its field is built fails: use valueOrNull or attached.
controller() returns the text controller of the handle, created on the first call: TextEditingController for
String, IntEditingController for int, DoubleEditingController for double (also nullable),
TextListEditingController for List<String>. Any other T throws UnsupportedError: pass a controllerFactory to
field().
Fields
Every field takes its handle as field: and an optional initialValue, that applies only when the handle has no
controller yet. Validators take the non-null value.
late final XFormField<int?> _age = field();
late final XFormField<bool> _terms = field();
late final XFormField<MaterialColor> _color = field();
late final XFormField<DateTime?> _birthday = field();
late final XFormField<TimeOfDay?> _alarm = field();
late final XFormField<XFile?> _photo = field();
XIntFormField(field: _age, min: 18, max: 120, arrows: const NumberFieldArrows(step: 5)),
XBoolFormField.switchbox(field: _terms, titleText: 'I accept', validator: (value) => value ? null : 'Required'),
XDropdownButtonFormField<MaterialColor>(field: _color, items: Colors.primaries),
XDateTimeFormField(field: _birthday, lastDate: DateTime.now()),
XTimeFormField(field: _alarm),
XFileFormField(field: _photo, mimeTypes: const {'image/*'}),
- Text:
XTextFormField(XFormField<String>). - Numbers:
XIntFormFieldandXDoubleFormField(XFormField<int?>,XFormField<double?>) takemin,maxandarrows;NumberFieldArrowssetsstep,onlyArrowsandroundToStep.IntFieldandDoubleFieldare the same inputs outside a form. - Bool:
XBoolFormField.checkboxand.switchbox. - Dropdown:
XDropdownButtonFormFieldandXDropdownMenuFormField(withmenuEntryByLabel). - Date and time:
XDateTimeFormField(firstDate,lastDate,includeTime,dateFormat) andXTimeFormField. - Files:
XFileFormField(XFormField<XFile?>) andXFilesFormField(XFormField<List<XFile>>) takemimeTypesandonMimeTypeNotMatch, and accept files dropped on them on the web.XDropViewis the standalone drop target.
Lists
XListFormField<E> shows a row per item with add and remove buttons; the itemBuilder gets the handle of each item.
XSetFormField<E> lets each row choose among items not chosen by the other rows. XListTabsFormField<E> puts each
item in a tab. XTextListFormField takes words typed in one text field and shows them as chips (List<String>).
XListFormField<String>(
field: _tags, // XFormField<List<String>>
decoration: const InputDecoration(labelText: 'Tags'),
maxLength: 3,
addButtonText: 'Add tag',
itemInitialValue: (values, index) => '',
validator: listUnique<String>(() => 'Duplicated tag'),
itemBuilder: (state, field, values, index) => XTextFormField(field: field, initialValue: values[index]),
),
Dynamic inner forms
ProxyFormField<T> computes its value from inner fields that live in an XFormFieldStore: create, remove, reorder and
read them by id.
ProxyFormField<List<String>>(
field: _phones, // XFormField<List<String>>
initialValue: const [],
initializeFields: (value, fields) => fields.addFields<String>({for (var i = 0; i < value.length; i++) '$i'}),
computeFieldData: (values) => values.values.cast<String>().toList(),
builder: (state, fields) => Column(
children: [
for (final id in fields.fields.keys) XTextFormField(field: fields.field<String>(id)),
TextButton(onPressed: fields.addUniqueField<String>, child: const Text('Add')),
],
),
),
The inner fields are validated, saved and reset with the outer form; computeErrorText builds the error from the
invalid ones. validateUnique and validateUniqueBy reject a value repeated among the fields of a store.
Validators
nonEmpty, nonBlank, nonNull, minLen, maxLen, matches, matchesEmail, contains, gt, gte, lt, lte,
oneOf, notIn, fieldEquals, fieldNotEquals, listNonEmpty, listUnique, listUniqueBy. Each takes a message
getter, so messages can be localized. Comparison and pattern validators accept a null value: pair them with nonNull.
validateFirst returns the first error of several validators; validateAll joins all errors (joiner prefixes each).
fieldEquals(_password, () => 'Different') compares with another handle, for a confirmation field.
Decorations
SingleSideInputBorder(side:, margin:) draws one line on a side of the field, as a border of the InputDecoration.
InputDecoratorLoader shows a progress indicator in the decorator while something loads. CustomContentInputDecorator
puts any widget inside a themed decorator, as the bool and the arrows do.
Behavior
resetrestores the value of the first construction, also after the form was rebuilt with anotherinitialValue.- The handle disposes the focus node and the controller it created, also those of a
controllerFactory; the controllers, scroll controllers and nodes you pass to a field stay yours to dispose. - Fields removed from an
XFormFieldStore(also on reset and with the state) are disposed by the store. - Dates are formatted with the locale of the context: for a locale other than
en_UScallinitializeDateFormattingfirst. The pickers start fromclock.now(), which tests control withwithClock. XDropView, and so the drop on the file fields, receives files only on the web; elsewhere the dialog is the way in.matchesandcontainsclash with the matchers offlutter_test: in tests import it withhide contains, matches.
Testing
Replace the file dialog with a FileSelectorPlatform fake (file_selector_platform_interface,
plugin_platform_interface): set FileSelectorPlatform.instance to a class that returns the chosen file from
openFile and openFiles. Fields are driven with tester.enterText, submitForm() and resetForm().
Claude Code plugin
The repository ships a plugin with skills that teach Claude Code to integrate the library:
/plugin marketplace add https://gitlab.com/dade.sliep/flutterx_forms
/plugin install flutterx-forms@flutterx-forms
Libraries
- flutterx_forms
- Typed Flutter form fields with validators, a form controller and the text, number, date, file, list and drop widgets built on them.