I have a scenario where I need to have a form with multiple TextFormField
s (in the snippet below, I simplified to three). These TextFormField
s 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
.
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
I think it is because you are using the nested
setState
in theonPresses
because the innersetState
call is redundant because it’s already within asetState
callback. You can simply it by having a singlesetState
.You can also initialize the
form
widget beforehand, conditionally based on_shouldSetFormVisible
as such :Updated ur code a lil bit, wrote comments too. Hope this may serve ur purposes