Monkey D. Teach
Monkey D. Teach

Reputation: 175

SetState called during build()

I wrote logic with edit mode which allows user to make changes in input field, but when edit mode button is clicked again then input need back to value before editing. And there is a problem with that, because everything works fine but console is showing me this error:

════════ Exception caught by foundation library ════════════════════════════════
The following assertion was thrown while dispatching notifications for TextEditingController:
setState() or markNeedsBuild() called during build.

This Form widget cannot be marked as needing to build because the framework is already in the process of building widgets.  A widget can be marked as needing to be built during the build phase only if one of its ancestors is currently building. This exception is allowed because the framework builds parent widgets before children, which means a dirty descendant will always be built. Otherwise, the framework might not visit this widget during this build phase.
The widget on which setState() or markNeedsBuild() was called was: Form-[LabeledGlobalKey<FormState>#bcaba]
    state: FormState#65267
The widget which was currently being built when the offending call was made was: ProfileInput
    dirty
    state: _ProfileInputState#32ea5

I know what this error means, but I can't find a place responsible for this. Could someone explain it to me?

class Profile extends StatefulWidget {
  @override
  _ProfileState createState() => _ProfileState();
}

class _ProfileState extends State<Profile> {
  GlobalKey<FormState> _formKey = GlobalKey<FormState>();
  User _user = User(
    username: "name",
  );
  String? _tmpUsername;
  bool _editMode = false;

  void _createTemporaryData() {
    _tmpUsername = _user.username;
  }

  void _restoreData() {
    _user.username = _tmpUsername!;
  }

  void _changeMode() {
    if (_editMode)
      _restoreData();
    else
      _createTemporaryData();
    setState(() {
      _editMode = !_editMode;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Column(
        children: [
          ElevatedButton(
              onPressed: () => _changeMode(), child: Text("change mode")),
          Form(
            key: _formKey,
            child: ProfileInput(
              editMode: _editMode,
              user: _user,
              onChangeName: (value) {
                _user.username = value;
              },
            ),
          ),
        ],
      ),
    );
  }
}

class ProfileInput extends StatefulWidget {
  final bool editMode;
  final User user;
  final void Function(String value)? onChangeName;

  ProfileInput({
    required this.editMode,
    required this.user,
    required this.onChangeName,
  });
  @override
  _ProfileInputState createState() => _ProfileInputState();
}

class _ProfileInputState extends State<ProfileInput> {
  TextEditingController _nameController = TextEditingController();

  @override
  void dispose() {
    _nameController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    _nameController.text = widget.user.username;
    return TextFormField(
      onChanged: widget.onChangeName,
      controller: _nameController,
      enabled: widget.editMode,
    );
  }
}

Upvotes: 1

Views: 1041

Answers (1)

Tirth Patel
Tirth Patel

Reputation: 5736

Put the following line in the initState or use addPostFrameCallback.

_nameController.text = widget.user.username; // goes into initState
  • initState
@override
void initState() {
  super.initState();
  _nameController.text = widget.user.username;
}
  • addPostFrameCallback
Widget build(BuildContext context) {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _nameController.text = widget.user.username;
  }); // 1

  SchedulerBinding.instance.addPostFrameCallback((_) {
    _nameController.text = widget.user.username;
  }); // 2

  // use either 1 or 2.
  
  // rest of the code, return statement.
}

Calling text setter on _nameController would notify all the listener and it's called inside the build method during an ongoing build that causes setState() or markNeedsBuild() called during build.

From Documentation:

Setting this will notify all the listeners of this TextEditingController that they need to update (it calls notifyListeners). For this reason, this value should only be set between frames, e.g. in response to user actions, not during the build, layout, or paint phases.

Upvotes: 1

Related Questions