skip to Main Content

I have a scenario where I need to have a form with multiple TextFormFields (in the snippet below, I simplified to three). These TextFormFields must be only shown after a certain Future completes (it is an animation, but I’m simplifying for easy reproducibility).

When the form is shown, the focus is automatically set to first TextFormField and each time the user inputs one character, the focus automatically moves to the next TextFormField. The last TextFormField, when filled, does not change focus.

The main problem is: when the keyboard "done" button is clicked, the cursor goes back to the first TextFormField and the keyboard shows back up (it starts animating to hide, but the animation does not complete and it goes back up (see gif below)). It also happens if the user tries to click "done" on keyboard even if it is not in last TextFormField.

enter image description here

See code below:

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({
    super.key,
  });

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool _isAnswerCorrect = false;
  bool _shouldSetFormVisible = false;

  Widget _form = Container();

  final _formKey = GlobalKey<FormState>();

  FocusNode _focusNode1 = FocusNode();
  FocusNode _focusNode2 = FocusNode();
  FocusNode _focusNode3 = FocusNode();

  TextEditingController _textEditingController1 = TextEditingController();
  TextEditingController _textEditingController2 = TextEditingController();
  TextEditingController _textEditingController3 = TextEditingController();

  void _answerIsCorrect() {
    setState(() {
      _isAnswerCorrect = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_shouldSetFormVisible) {
      setState(() {
        _shouldSetFormVisible = false;
      });

      //I tried moving the assignment of variable _form to here so that the color of font of the form would change on button submit (according to this answer: https://stackoverflow.com/questions/79100177/form-set-after-future-delayed-wont-rebuild-after-subsequent-state-change-of-var), but now, after adding _shouldSetFormVisible to only assign the form field once, it will not change color anymore. I believe it was working because the form variable was being reassigned at each rebuild, which shoudn't be necessary (see the second code snippet I provided on the link I shared above)

      _form = SizedBox(
        width: 100,
        child: Column(
          children: [
            Form(
              key: _formKey,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Expanded(
                    child: NumberInputField(
                        isAnswerCorrect: _isAnswerCorrect,
                        isFirstField: true,
                        isLastField: false,
                        currentFieldFocusNode: _focusNode1,
                        nextFieldFocusNode: _focusNode2,
                        textEditingController: _textEditingController1),
                  ),
                  SizedBox(width: 20),
                  Expanded(
                    child: NumberInputField(
                        isAnswerCorrect: _isAnswerCorrect,
                        isFirstField: false,
                        isLastField: false,
                        currentFieldFocusNode: _focusNode2,
                        nextFieldFocusNode: _focusNode3,
                        textEditingController: _textEditingController2),
                  ),
                  SizedBox(width: 20),
                  Expanded(
                    child: NumberInputField(
                        isAnswerCorrect: _isAnswerCorrect,
                        isFirstField: false,
                        isLastField: true,
                        currentFieldFocusNode: _focusNode3,
                        nextFieldFocusNode:
                            _focusNode3, //tried setting to another focusNode, but the result is the same
                        textEditingController: _textEditingController3),
                  ),
                ],
              ),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              child: Text('Submit'),
              onPressed: _answerIsCorrect,
            ),
          ],
        ),
      );
    }

    return Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              child: const Text('Show form'),
              onPressed: () {
                Future.delayed(
                  const Duration(milliseconds: 100),
                  () => setState(
                    () {
                      setState(() {
                        _shouldSetFormVisible = true;
                      });
                    },
                  ),
                );
              },
            ),
            const SizedBox(height: 20),
            _form,
          ],
        ),
      ),
    );
  }
}

class NumberInputField extends StatelessWidget {
  final bool isAnswerCorrect;

  final bool isFirstField;
  final bool isLastField;
  final FocusNode currentFieldFocusNode;
  final FocusNode nextFieldFocusNode;
  final TextEditingController textEditingController;

  const NumberInputField({
    super.key,
    required this.isAnswerCorrect,
    required this.isFirstField,
    required this.isLastField,
    required this.currentFieldFocusNode,
    required this.nextFieldFocusNode,
    required this.textEditingController,
  });

  @override
  Widget build(BuildContext context) {
    Color fontColor = Colors.blue;

    if (isAnswerCorrect) {
      fontColor = Colors.black;
    }

    //focus on first field automatically
    if (isFirstField) {
      FocusScope.of(context).requestFocus(currentFieldFocusNode);
    }

    return TextFormField(
      controller: textEditingController,
      focusNode: currentFieldFocusNode,
      onChanged: (val) {
        //if user inputs one char, goes to next field
        if (val.length == 1) {
          if (!isLastField) {
            FocusScope.of(context).requestFocus(nextFieldFocusNode);
          }
          //the else statement below also causes the form to be rebuild (cursor goes back to first field)
          // else {
          //   FocusScope.of(context).unfocus();
          // }
        }
      },
      style: TextStyle(color: fontColor),
      keyboardType: TextInputType.number,
      textAlign: TextAlign.center,
      decoration: const InputDecoration(
        hintText: '?',
      ),
    );
  }
}


It looks like the form is "rebuilding" somehow (which btw, I don’t understant, i.e., which means for a form to rebuild and when this happens, because it only change some of its state, e.g. the numbers typed don’t disappear)

Also, another, but probably related, problem is that the color of the font color of the form won’t change after ElevatedButton is pressed. Please see, for this other problem, the question I asked here.

2

Answers


  1. I think it is because you are using the nested setState in the onPresses because the inner setState call is redundant because it’s already within a setState callback. You can simply it by having a single setState.

    onPressed: () => setState(() {
       _shouldSetFormVisible = true;
    }),
    

    You can also initialize the form widget beforehand, conditionally based on _shouldSetFormVisible as such :

    Widget _form = _shouldSetFormVisible ? buildForm() : Container();
    
    Login or Signup to reply.
  2. Updated ur code a lil bit, wrote comments too. Hope this may serve ur purposes

    class MyHomePage extends StatefulWidget {
      const MyHomePage({super.key});
      @override
      State<MyHomePage> createState() => _MyHomePageState();
    }
    
    class _MyHomePageState extends State<MyHomePage> {
      final _formKey = GlobalKey<FormState>();
      final _focusNode1 = FocusNode();
      final _focusNode2 = FocusNode();
      final _focusNode3 = FocusNode();
      final _textEditingController1 = TextEditingController();
      final _textEditingController2 = TextEditingController();
      final _textEditingController3 = TextEditingController();
      bool _shouldSetFormVisible = false;
    
      void _onSubmit() {
        if (!_formKey.currentState!.validate()) return;
        String confirmedOTP = _textEditingController1.text +
            _textEditingController2.text +
            _textEditingController3.text;
        // do what u wanna do when submitted
        print(confirmedOTP);
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  child: const Text('Show form'),
                  onPressed: () {
                    Future.delayed(
                      const Duration(milliseconds: 100),
                      () {
                        setState(() => _shouldSetFormVisible = true);
                        // setting initial focusNode
                        FocusScope.of(context).requestFocus(_focusNode1);
                      },
                    );
                  },
                ),
                const SizedBox(height: 20),
                if (_shouldSetFormVisible) _getForm,
              ],
            ),
          ),
        );
      }
    
      Widget get _getForm => SizedBox(
            width: 150,
            child: Column(
              children: [
                Form(
                  key: _formKey,
                  child: Row(
                    mainAxisAlignment: MainAxisAlignment.center,
                    children: [
                      Expanded(
                        child: NumberInputField(
                            currentFieldFocusNode: _focusNode1,
                            nextFieldFocusNode: _focusNode2,
                            textEditingController: _textEditingController1),
                      ),
                      SizedBox(width: 20),
                      Expanded(
                        child: NumberInputField(
                          currentFieldFocusNode: _focusNode2,
                          nextFieldFocusNode: _focusNode3,
                          textEditingController: _textEditingController2,
                        ),
                      ),
                      SizedBox(width: 20),
                      Expanded(
                        child: NumberInputField(
                          currentFieldFocusNode: _focusNode3,
                          nextFieldFocusNode: null,
                          textEditingController: _textEditingController3,
                        ),
                      ),
                    ],
                  ),
                ),
                SizedBox(height: 20),
                ElevatedButton(
                  onPressed: _onSubmit,
                  child: Text('Submit'),
                ),
              ],
            ),
          );
    
      @override
      void dispose() {
        _focusNode1.dispose();
        _focusNode2.dispose();
        _focusNode3.dispose();
        _textEditingController1.dispose();
        _textEditingController2.dispose();
        _textEditingController3.dispose();
        super.dispose();
      }
    }
    
    class NumberInputField extends StatelessWidget {
      final FocusNode currentFieldFocusNode;
      final FocusNode? nextFieldFocusNode;
      final TextEditingController textEditingController;
      const NumberInputField({
        super.key,
        required this.currentFieldFocusNode,
        this.nextFieldFocusNode,
        required this.textEditingController,
      });
    
      @override
      Widget build(BuildContext context) {
        return TextFormField(
          controller: textEditingController,
          focusNode: currentFieldFocusNode,
          keyboardType: TextInputType.number,
          textInputAction: nextFieldFocusNode == null
              ? TextInputAction.done
              : TextInputAction.next,
          textAlign: TextAlign.center,
          style: TextStyle(height: 0, color: Colors.blue),
          // setiing maximum length of each field is 1
          inputFormatters: [LengthLimitingTextInputFormatter(1)],
          decoration: InputDecoration(
            hintText: '?',
            fillColor: Colors.transparent,
            contentPadding: EdgeInsets.symmetric(vertical: 20),
    
            ///
            /// u can edit these functionalities as u want
            focusedBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.blue, width: 2),
            ),
            enabledBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.blue, width: 2),
            ),
            errorBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.red, width: 2),
            ),
            focusedErrorBorder: UnderlineInputBorder(
              borderSide: BorderSide(color: Colors.red, width: 2),
            ),
            errorStyle: const TextStyle(height: 0),
    
            ///
            ///
          ),
          onChanged: (val) {
            if (val.isEmpty) {
              // if we remove a OTP-text, we may wanna stay on the same field, so doing nothing
            } else if (nextFieldFocusNode != null) {
              // goint to next-Field if it's not the last OTP-Field
              FocusScope.of(context).nextFocus();
            } else if (nextFieldFocusNode == null) {
              // for last OTP-Field, we r removing focus automatically
              FocusManager.instance.primaryFocus?.unfocus();
            } else {
              FocusScope.of(context).canRequestFocus;
            }
          },
          validator: (value) {
            // write error text here
            if (value == null || value.isEmpty) return 'write error text';
            return null;
          },
          // if user press somewhere but on textfield then keyboard & focus dismissed
          onTapOutside: (_) => FocusManager.instance.primaryFocus?.unfocus(),
        );
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search