Reusable field widgets

The package ships no styled widgets on purpose. Extract each binding once, restyle it to your design system, and reuse it across every form.

The package deliberately ships no styled widgets — it gives you what a widget needs to bind to: the text controller, the focus node, the typed state, the error. A design system already has the inputs; what it lacks is the five lines of wiring, and those are written once.

The extract-once pattern

A reusable field widget takes the field and an error translator, and nothing else the controller already knows:

class AppTextField<E extends Object> extends StatelessWidget {
  const AppTextField({
    super.key,
    required this.field,
    required this.translateError,
    this.label,
    this.hint,
    this.obscureText = false,
  });

  final AdvancedTextFieldController<E> field;
  final ErrorTranslator<E> translateError; // String Function(E)
  final String? label;
  final String? hint;
  final bool obscureText;

  @override
  Widget build(BuildContext context) {
    return AdvancedFieldBuilder<String, E>(
      field: field,
      builder: (context, state, _) {
        final error = state.error; // promote E? to E before translating
        return TextFormField(
          controller: field.textController,
          focusNode: field.focusNode,
          obscureText: obscureText,
          readOnly: state.readOnly,
          enabled: !state.readOnly,
          decoration: InputDecoration(
            labelText: label,
            hintText: hint,
            errorText: error == null ? null : translateError(error),
            suffixIcon: state.isInProgress ? const _Spinner() : null,
          ),
        );
      },
    );
  }
}

Being generic in E is what lets one widget serve every form in the app, whatever error type each one chose. A generic select widget binds AdvancedFieldBuilder<V?, E> over AdvancedSingleSelectFieldController<V, E> the same way; the Rendering fields page has a working set of five.

Where the translator lives

ErrorTranslator<E> is String Function(E) — a plain function, so put it where the wording belongs. One shared translator per error enum is the cheapest start:

String translate(SignupError error) => switch (error) {
  SignupError.required => 'This field is required',
  SignupError.tooShort => 'At least 8 characters',
  SignupError.taken => 'Already registered',
};

The same code often wants different wording per field — "Required" versus "Pick a country" — so the widget takes the function rather than the enum, and a call site can pass a field-specific one. In a localised app the translator calls AppLocalizations.of(context), which is why it is a parameter of the widget and not a method on the error.

The docs' own shorthand

The live examples on this site use DocsTextField, DocsDropdownField, DocsSubmitButton and friends, so a page about validation does not re-teach binding before it gets to the point. They are exactly the pattern above — generic in E, bound to the field's controllers, callbacks nulled while read-only — and every example that uses one says so under its code. They are not part of the package; copy the idea, not the import.

Ready to copy

example/lib/widgets/ in the repository has the example app's set: a text field, a password field with a list-typed error, a dropdown, a switch, and three variants that demonstrate the child: optimisation with a leading icon, an avatar card and a banner. Each is a plain StatelessWidget over AdvancedFieldBuilder, ready to restyle.

The example app itself is a gallery: one screen per documented pattern.

Next

On this page