fluttertextfieldonchangeflutter-provider

Why is the cursor in a TextField jumping to the beginning of the text whenever the onChanged event is triggered?


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'),
                ),
              ),

Solution

  • 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: