Forms, Input & Focus
Irbisa · cheatsheetSeptember 13, 2026

Forms, Input & Focus

TextField, TextEditingController, Form validation, FocusNode, input formatters, keyboard insets

Junior Developer16 itemscompressed for a skim
  1. 01

    Every keystroke in a TextField vanishes as soon as the parent rebuilds. What do you look at first?

    Easy

    A TextEditingController constructed inside build() — every rebuild hands the field a fresh, empty controller, and the old text goes with the old object.

    // ❌ a new controller on every build — the text resets and nothing disposes it
    class BadForm extends StatelessWidget {
      const BadForm({super.key});
    
      @override
      Widget build(BuildContext context) {
    …
  2. 02

    A search field wired with controller.addListener fires a query when the user only moves the caret — how do onChanged, onSubmitted and a controller listener differ?

    Easy

    onChanged fires only on user edits, onSubmitted only when the user presses the keyboard's action key, and a controller listener fires on every change to the whole TextEditingValue — caret moves included.

    class _SearchState extends State<Search> {
      final _query = TextEditingController();
      String _lastQuery = '';
      bool _typing = false;
      Timer? _debounce;
    …
  3. 03

    Showing an error under one field pushes the rest of the form down a few pixels. Where do label, hint, helper and error text actually live?

    Easy

    All four are painted by InputDecorator inside the field's own box, and errorText adds a subtext line that was not there before, so the field grows and everything below it shifts.

    TextFormField(
      decoration: const InputDecoration(
        labelText: 'Email',            // floats up on focus or when filled
        hintText: 'you@example.com',   // visible only while empty
        helperText: ' ',               // reserves the subtext line: no jump later
        prefixIcon: Icon(Icons.alternate_email),
    …
  4. 04

    You set textInputAction: TextInputAction.next and the key does nothing. What do keyboardType, textInputAction and textCapitalization actually control?

    Easy

    All three are hints sent to the platform's IME, and the next key only moves focus while the framework's default onEditingComplete is in place — supply your own callback and you have replaced it.

    Column(
      children: [
        TextField(
          focusNode: _emailNode,
          keyboardType: TextInputType.emailAddress,
          textInputAction: TextInputAction.next,   // default handler: nextFocus()
    …
  5. 05

    You call _formKey.currentState!.save() and the model you meant to fill is still empty. Where do a Form's values actually live?

    Medium

    Each FormField keeps its own value in its FormFieldState; the Form holds nothing but the list of fields that registered with it, and save() merely calls their onSaved callbacks — with no onSaved, it does nothing at all.

    class _CheckoutState extends State<Checkout> {
      final _formKey = GlobalKey<FormState>();
      Order _order = const Order();      // filled by onSaved, not by save() itself
    
      void _submit() {
        final form = _formKey.currentState!;   // null before the first build
    …
  6. 06

    A sign-up form opens covered in red "Email is required" before the user has typed a character. Which autovalidateMode is that, and what would you use instead?

    Medium

    AutovalidateMode.always — it runs every validator on every build, the first one included, so an untouched empty form is already full of errors.

    // ❌ every validator runs on the first build — the form opens in red
    Form(
      key: _formKey,
      autovalidateMode: AutovalidateMode.always,
      child: Column(children: _fields),
    );
    …
  7. 07

    On a login screen, tapping the background never closes the keyboard and the next key skips a field. How does focus work and who owns the nodes?

    Medium

    Focus lives in its own tree of FocusNodes alongside the widget tree — a TextField merely attaches to a node, so the node is created and disposed by the State, and on touch platforms nothing dismisses the keyboard unless you ask for it.

    class _LoginState extends State<Login> {
      final _email = FocusNode();
      final _password = FocusNode();
    
      @override
      void dispose() {
    …
  8. 08

    Product wants "that username is taken" under the field while the user types, but a FormFieldValidator has to return synchronously. How do you wire it?

    Medium

    Keep the network call out of the validator: debounce onChanged, park the server's answer in the State, and let the synchronous validator return the message that is already sitting there.

    class _UsernameFieldState extends State<UsernameField> {
      final _controller = TextEditingController();
      Timer? _debounce;
      int _request = 0;
      String? _taken;        // the message the synchronous validator will hand back
      bool _checking = false;
    …
  9. 09

    Your thousands-separator formatter produces the right text, but the caret jumps to the end of the field after every keystroke — where is the bug?

    Medium

    A TextInputFormatter returns a whole TextEditingValue, so the moment you rebuild the text you are also declaring where the caret goes — and the usual guess is text.length.

    class ThousandsFormatter extends TextInputFormatter {
      static final _digit = RegExp(r'[0-9]');
    
      @override
      TextEditingValue formatEditUpdate(TextEditingValue old, TextEditingValue next) {
        final digits = next.text.replaceAll(RegExp(r'[^0-9]'), '');
    …
  10. 10

    On a small phone the keyboard covers your Continue button and the field beneath it — what does Scaffold already do here, and what is left for you?

    Medium

    Scaffold already shrinks the body by the keyboard height — resizeToAvoidBottomInset defaults to true — but it cannot scroll a body you built as a fixed Column, so whatever no longer fits overflows or hides.

    // ❌ nothing can scroll, so the resize just squeezes the Column
    Scaffold(
      body: Column(
        children: [
          TextField(controller: _email),
          const Spacer(),
    …
  11. 11

    The OS password manager never offers to fill or save your login form, and the SMS code still has to be typed by hand — what is missing?

    Medium

    Autofill is a contract with the platform: the fields must carry autofillHints, live inside an AutofillGroup, and the context must be finished — and for the right saved credential to appear, the app has to be linked to your domain.

    AutofillGroup(                       // these fields are ONE credential
      child: Column(
        children: [
          TextField(
            controller: _email,
            autofillHints: const [AutofillHints.username], // not .email
    …
  12. 12

    Your date picker sits inside a Form but validate() ignores it and save() never sees its value — how do you make a non-text widget a real form field?

    Medium

    Subclass FormField<T>: it owns the value, the error text and — crucially — the registration with the enclosing FormState, which is the part a hand-rolled widget never does.

    class DateFormField extends FormField<DateTime> {
      DateFormField({
        super.key,
        super.initialValue,
        super.validator,
        super.onSaved,
    …
  13. 13

    A search field driven by a Bloc drops characters when you type fast and the cursor keeps jumping to the end — who is supposed to own the text?

    Medium

    The TextEditingController owns the text and the caret; if you assign controller.text from every emitted state you are throwing the caret away and racing the keyboard.

    // ❌ the bloc echoes the text back and fights the keyboard
    BlocBuilder<SearchBloc, SearchState>(
      builder: (context, state) {
        _ctrl.text = state.query;        // caret to the end on every keystroke
        return TextField(
          controller: _ctrl,
    …
  14. 14

    A user rotates the phone half-way through a signup form and everything typed is gone — what actually got destroyed, and what would have restored it?

    Hard

    Rotation is not the culprit: a configuration change in Flutter is just a resize, the isolate and every State object survive it.

    class _SignupPageState extends State<SignupPage> with RestorationMixin {
      // Survives a rebuild AND process death; a plain controller only survives rebuilds.
      final RestorableTextEditingController _email = RestorableTextEditingController();
      final RestorableBool _newsletter = RestorableBool(false);
    
      @override
    …
  15. 15

    A 30-field form built with ListView.builder loses typed values and error messages once a field scrolls off screen — what causes it and what do you change?

    Hard

    ListView.builder unmounts children that scroll out of the cache extent, and everything those Elements owned goes with them — the row's TextEditingController and its FormFieldState.

    // ❌ the controller lives in the row and dies when the row scrolls away
    class _RowState extends State<FieldRow> {
      final _ctrl = TextEditingController();      // disposed with the Element
      @override
      Widget build(BuildContext context) =>
          TextFormField(controller: _ctrl, validator: widget.validator);
    …
  16. 16

    Your uppercase-everything input formatter breaks Japanese typing and chops emoji in half — what is TextEditingValue.composing and why does rewriting text corrupt it?

    Hard

    While an IME is composing, the field holds provisional text marked by TextEditingValue.composing, a range the platform owns; rewrite the text underneath it and Flutter and the IME stop agreeing about what is on screen.

    // ❌ rewrites text mid-composition and keeps a range that no longer fits it
    class ShoutFormatter extends TextInputFormatter {
      @override
      TextEditingValue formatEditUpdate(TextEditingValue old, TextEditingValue next) {
        return TextEditingValue(
          text: next.text.toUpperCase(),
    …