Field controllers

The four controllers

Text, boolean, single-select and multi-select — constructors, setters, and the methods they all share.

ControllerValueConstructorSetters
AdvancedTextFieldController<E>String{initialValue = '', validator, asyncValidation, focusNode, name}typing into textController, setValue
AdvancedBooleanFieldController<E>bool{initialValue = false, validator, asyncValidation, focusNode, name}setValue
AdvancedSingleSelectFieldController<V, E>V?{required V? initialValue, required List<V> options, validator, asyncValidation, focusNode, name}select(V? option); null clears; setOptions(List<V>)
AdvancedMultiSelectFieldController<V, E>Set<V>{required Set<V> initialValue, required List<V> options, validator, asyncValidation, focusNode, name}toggleElement, addValue, removeValue, setOptions(List<V>)

All of them support reset(), markReadOnly() / unmarkReadOnly(), setError(), clearErrors(), setValidationMode(), subscribeToFields(), validate(), getValueSetter(), focus(), and both sync and async validation. name labels the field in reported errors and as its FocusNode's debug label; it is not used for identity — fields are identified by reference.

pizza_order.dartidle
Source2 files
pizza_order_controller.dart
enum Size { small, medium, large }

enum Topping { mozzarella, basil, olives, mushrooms, pepperoni }

class PizzaOrderController extends AdvancedFormController {
  PizzaOrderController() {
    registerFields([size, toppings, extraCrispy, notes]);
  }

  // A required dropdown: null to start, notNull to enforce a choice.
  final size = AdvancedSingleSelectFieldController<Size, String>(
    initialValue: null,
    options: Size.values,
    validator: notNull('Pick a size'),
  );

  // A Set value; there is no built-in Set validator, so a closure.
  final toppings = AdvancedMultiSelectFieldController<Topping, String>(
    initialValue: const {Topping.mozzarella},
    options: Topping.values,
    validator: (chosen) => chosen.length > 3 ? 'At most 3 toppings' : null,
  );

  final extraCrispy = AdvancedBooleanFieldController<String>();

  final notes = AdvancedTextFieldController<String>(
    validator: notLongerThan(60, 'Keep it short'),
  );

  String get summary =>
      '${size.fieldValue?.name} with ${toppings.fieldValue.map((t) => t.name).join(', ')}'
      '${extraCrispy.fieldValue ? ', extra crispy' : ''}';
}
DocsActions, DocsChipsField, DocsDropdownField, DocsSubmitButton, DocsSwitchField and DocsTextField are shorthands these docs define, not part of the package. See Rendering fields for the widget code an app writes.

Text

Owns a TextEditingController — field.textController — kept in two-way sync with the value: user input flows into the field, and programmatic changes (setValue, reset, prefill, a relation) flow back into the visible text, with the caret kept on the same characters where possible. Text typed into a read-only field is reverted in the same turn.

Boolean

Defaults to false. The one rule a boolean usually needs is built in: validator: mustBeTrue(MyError.mustAccept).

Single select

Holds a V? and a non-nullable List<V> options. A required dropdown is initialValue: null plus validator: notNull(MyError.required). select(option) asserts in debug builds that the option is one of options; select(null) clears and is always allowed. initialValue is never checked, so an off-list initial value is the way to represent "unknown".

The list can change while the form is open — instructors filtered by the chosen aircraft, cities loaded once the country is known. setOptions(newList) replaces it and clears a selected value that is not on the new list. The clearing is the program's doing, like prefill: an untouched field stays untouched and shows no error until validate(), while a field the user had picked from shows its notNull error at once. A read-only field is cleared too, since a value that is not on the list cannot be shown. Widgets rebuild even when the value stayed the same.

aircraft.addListener(() {
  instructor.setOptions(instructorsFor(aircraft.fieldValue));
});

Membership is ==

select, addValue and setOptions compare with ==. A Pilot deserialized from the server is a different object from the Pilot on the list, so with the default identity == every refresh clears the selection. Make V a value type (Equatable, a record, an enum) or select by id and resolve to the instance that is on the list.

Multi select

Holds a Set<V>. toggleElement(v) adds or removes; addValue asserts membership in options, removeValue stays silent for an off-list value. setOptions(newList) replaces the list and drops the selected values that are not on it, with the same rules as on the single select. Both controllers copy the initialValue and the options you pass, so mutating them later never reaches the field.

Reading a field

The full state is field.value, an AdvancedFieldState<T, E>. Two shortcuts cover most reads:

field.fieldValue; // the current value — short for field.value.value
field.error;      // the current error, or null — short for field.value.error

Next

On this page