Skip to main content

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 a MutableValue<String>. A useFieldState (which is a MutableValue<String>) is the typical source.
  • builder - receives the managed TextEditingController; pass it to your TextField / TextFormField.
  • controllerProvider - factory for the controller, defaulting to TextEditingController.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 TextField to a useFieldState so the field's value participates in your state (validation, derived enablement, submit) without you holding a TextEditingController and 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.value moves the cursor. The effect assigns controller.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 through text from outside - let the user's own typing flow through the controller, and reserve text.value = for deliberate, non-per-keystroke changes.

  • text and the controller stay synced both ways, so don't also hold a separate TextEditingController for the same field. The value lives in text; 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 the useFieldState (or other MutableValue<String>) you passed in; this widget does not surface errors.

  • StatelessTextEditingControllerWrapper is deprecated - it is a typedef alias for this class. Use TextEditingControllerWrapper directly.

See also