My app has (among other things) a TextField
and ElevatedButton
. I'm managing state with Provider
. I want the button color to change from white to blue when the TextField's content changes.
I have this code in the TextField:
onChanged: (_) {
context.read<Data>().changeButtonColor();
},
This in the ElevatedButton:
style: ElevatedButton.styleFrom(
backgroundColor: context.watch<Data>().buttonColor),
And this in the Provider:
Color buttonColor = Colors.white;
void changeButtonColor() {
buttonColor = Colors.blue;
notifyListeners();
}
The button's color changes as expected. The problem I have, however, is that the cursor in the TextField
jumps to the beginning of the text as soon as the onChanged
event is triggered. That makes the app unusable.
Why does the cursor do this? And how do I make the code work as desired, i.e., without the TextField
cursor jumping to the beginning of the text?
EDIT: the parent build()
method includes
Note? selNote = context.watch<Data>().selectedNote;
noteTextController.text = selNote.content;
For reference, here's the complete code for the two widgets:
// Note Text TextField---------------------------------
Padding(
padding: EdgeInsets.only(
top: 15,
bottom: 15,
),
child: TextField(
controller: noteTextController,
keyboardType: TextInputType.multiline,
maxLines: null,
minLines: 10,
decoration: InputDecoration(
border: InputBorder.none,
fillColor: Colors.white,
),
onChanged: (_) {
context.read<Data>().changeButtonColor();
},
),
),
// Save Button------------------------------------------
Padding(
padding: EdgeInsets.only(
top: 15,
bottom: 15,
),
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: context.watch<Data>().buttonColor),
onPressed: () async {
selNote = context.read<Data>().selectedNote;
var title = noteTitleController.text;
var content = noteTextController.text;
// If this is a new note ... create it
if (selNote == null) {
context.read<Data>().add(title, content);
} else {
// If this is an already existing note ... update it
selNote!.title = title;
selNote!.content = content;
context.read<Data>().update(title: title, content: content);
}
},
child: Text('Save'),
),
),
The problem is that you're changing the controller's text inside the build method. Every time your TextField
calls the onChanged
callback, the value of button color in the provider will be updated so when it's being watched in the same widget, it will retrigger the build
method which causes the controller's text to be updated, hence the cursor jump.
A TextField
's value should be local, and you shouldn't manually handle the text
change if the source of the change is from the TextField
itself. Just rely on the internal change from the controller, and if you want to "mirror" the state to a more global state, you can keep the onChanged
callback. You don't need to read back the value to the controller.
More generally, you shouldn't call the controller.text
setter in a declarative/reactive manner. It should only be called in an imperative manner.
Some related articles: