TextEditingControllerWrapper
Owns a TextEditingController and keeps it in two-way sync with a MutableValue<String>. It creates and disposes the controller, writes the value as the user types, and updates the field when the value changes from elsewhere - so a TextField can be backed by plain state (a useFieldState) without managing the controller's lifecycle.
class SearchField extends HookWidget {
const SearchField({super.key});
Widget build(BuildContext context) {
// useFieldState() owns the String; the wrapper binds a TextEditingController to it.
final query = useFieldState();
return Column(
children: [
TextEditingControllerWrapper(
text: query,
builder: (controller) => TextField(
controller: controller,
decoration: const InputDecoration(hintText: 'Search'),
),
),
// Reading query.value rebuilds as the user types; setting it updates the field.
Text('Searching for: ${query.value}'),
],
);
}
}
Constructor
TextEditingControllerWrapper({
Key? key,
required MutableValue<String> text,
required Widget Function(TextEditingController controller) builder,
TextEditingController Function({String? text}) controllerProvider = TextEditingController.new,
});
text- the source of truth for the field's contents, as aMutableValue<String>. AuseFieldState(which is aMutableValue<String>) is the typical source.builder- receives the managedTextEditingController; pass it to yourTextField/TextFormField.controllerProvider- factory for the controller, defaulting toTextEditingController.new. Override to construct a subclass.
The controller is created once with the current text.value and disposed automatically. A listener writes text from controller.text as the user types (skipping the write when they already match, to avoid an echo loop); an effect sets controller.text whenever text.value changes from outside and differs.
Use cases
- Binding a
TextFieldto auseFieldStateso the field's value participates in your state (validation, derived enablement, submit) without you holding aTextEditingControllerand disposing it. - Programmatically setting or clearing a field - a "clear" button, prefilling from a fetched value - by writing
text.value, which flows into the controller.
Caveats
-
Writing
text.valuemoves the cursor. The effect assignscontroller.text, which resets the selection to the end. That is invisible for external updates (clear, prefill) but means you should not push every keystroke back throughtextfrom outside - let the user's own typing flow through the controller, and reservetext.value =for deliberate, non-per-keystroke changes. -
textand the controller stay synced both ways, so don't also hold a separateTextEditingControllerfor the same field. The value lives intext; read it there. Keeping a parallel controller defeats the binding. -
For validation and error display, the field's value is just
text.value- the wrapper only binds text. Drive validation off theuseFieldState(or otherMutableValue<String>) you passed in; this widget does not surface errors. -
StatelessTextEditingControllerWrapperis deprecated - it is a typedef alias for this class. UseTextEditingControllerWrapperdirectly.
See also
- useFieldState - the form-field state usually passed as
text - useGenericFieldState - the typed field state behind
useFieldState - PageControllerWrapper - the same controller-binding pattern for
PageView - TabControllerWrapper - the same pattern for
TabBar/TabBarView