Flutter Not Calling Build in a Widget After setState() Despite a Change in State

91 views Asked by At

I have called setState() to update the objects of the class MyErrorBox, but Flutter does not run the build function for those objects. I am new to Flutter and coding in general. The MyText class in the code below is just a modified TextField and MySubmit is simply a button (newFunc is the function passed in).

Here is the code for the first file (with unimportant pieces cut out):

//assume proper imports

class CreateAccountWidget extends StatefulWidget {
  static List<String> errorMessages = [];

  const CreateAccountWidget({super.key});

  @override
  State<CreateAccountWidget> createState() => _CreateAccountWidgetState();

  static void updateErrorM(List<String> erm) {
    errorMessages = erm;
    print(errorMessages);
  }
}

class _CreateAccountWidgetState extends State<CreateAccountWidget> {

  //these editors control each of the textboxes
  TextEditingController editor1 = TextEditingController();
  TextEditingController editor2 = TextEditingController();
  TextEditingController editor3 = TextEditingController();

 //em is the list of error messages in the form of Strings
  static List<String> em = [];
  List<MyErrorBox> errors = [];

  List<MyErrorBox> possibleErrs = [
    MyErrorBox(whichError: 0, key: ValueKey("error1")),
    MyErrorBox(whichError: 1, key: ValueKey("error2")),
    MyErrorBox(whichError: 2, key: ValueKey("error3")),
    MyErrorBox(whichError: 3, key: ValueKey("error4"))
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Stack(
          children: [
            Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                MyText(c: editor1, h: "Enter a username", o: false),
                MyText(c: editor2, h: "Enter a password", o: true),
                MyText(c: editor3, h: "Confirm password", o: true),
                MySubmit(
                    ht: "btn7",
                    newFunc: () {
                      updateErrors(); // calling update errors here to rebuild screen
                      if (errors.isEmpty) {
                        FirebaseAuth.instance.createUserWithEmailAndPassword(
                            email: editor1.text, password: editor2.text);
                        Navigator.pushReplacementNamed(context, '/login');
                      }
                    }, 
                ),
              ],
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: errors, //this is where the errors are displayed
              ),
            )
          ],
        ),
      ),
    );
  }

//this is where I update the errors (and where I think the problem is)
  void updateErrors() {
    errors = [];
    em = [];
    print("");
    print("NEW UPDATE ERROR----------------------------");

    if (editor2.text != editor3.text) {
      em.add("Confirmed password must match");
    }
    if (editor1.text.isEmpty) {
      em.add("The username field cannot be empty");
    }
    if (editor2.text.isEmpty) {
      em.add("The password field cannot be empty");
    }
    if (editor1.text.length < 8) {
      em.add("The username must be at least 8 letters");
    }

    for (int i = 0; i < em.length; i++) {
      errors.add(possibleErrs[i]);
    }

    setState(() {
      CreateAccountWidget.updateErrorM(em);
    });

    print("");
  }
}

class MyErrorBox extends StatefulWidget {
  final int whichError;

  MyErrorBox({super.key, required this.whichError});

  @override
  State<MyErrorBox> createState() => MyErrorBoxState();
}

class MyErrorBoxState extends State<MyErrorBox> {
  late final int et;
  List<String> someL = CreateAccountWidget.errorMessages;

  @override
//updates the variable et
  void initState() {
    // TODO: implement initState
    super.initState();
    et = widget.whichError;
  }

  @override
  Widget build(BuildContext context) {
    someL = CreateAccountWidget.errorMessages;
    print("IN THE BUILD METHOD:");
    print(someL[et]);
    print("");
    return SizedBox(
      width: 280,
      height: 55,
      child: DecoratedBox(
        decoration: const BoxDecoration(color: Color.fromRGBO(255, 0, 0, .7)),
        child: Center(
          child: Text(
            someL[et],
            style: const TextStyle(
              color: Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}

When I run the program and it makes the error boxes, the initial errors are correct. After that, the code only runs the build method for any additional errors added to the errors list, and ignores rebuilding the first part of the list, resulting in the first errors not getting updated.

I expected Flutter to rebuild all the items in the Column list because there was a change of state for those errors, but it did not. I'm not sure if this has been answered before, because all of the other questions I have seen in relation to this issue were because the users were trying to update the code in the initState() method.

2

There are 2 answers

1
INeedHelp1234 On

Here is the implemented solution:

//code for the MyErrorBox class: 

class MyErrorBox extends StatefulWidget {
  final int whichError;
  final List<String> listErrors;

  MyErrorBox({super.key, required this.whichError, required this.listErrors});

  @override
  State<MyErrorBox> createState() => MyErrorBoxState();

  void printErrorText() {
    print(CreateAccountWidget.errorMessages[whichError]);
  }
}

class MyErrorBoxState extends State<MyErrorBox> {
  late String et;

  @override
  void initState() {
    // TODO: implement initState
    super.initState();
    et = widget.listErrors[widget.whichError];
    print("INSIDE THE INIT");
    print("");
  }

  @override
  Widget build(BuildContext context) {
    print("THIS IS AN ERROR BOX:");
    print(et);
    print("");
    return SizedBox(
      width: 280,
      height: 55,
      child: DecoratedBox(
        decoration: const BoxDecoration(color: Color.fromRGBO(255, 0, 0, .7)),
        child: Center(
          child: Text(
            et,
            style: const TextStyle(
              color: Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}



  //code for the CreateAccountWidget Class

class CreateAccountWidget extends StatefulWidget {
  static List<String> errorMessages = [];

  const CreateAccountWidget({super.key});

  @override
  State<CreateAccountWidget> createState() => _CreateAccountWidgetState();

  static void updateErrorM(List<String> erm) {
    errorMessages = erm;
    print(errorMessages);
  }
}

class _CreateAccountWidgetState extends State<CreateAccountWidget> {
  TextEditingController editor1 = TextEditingController();
  TextEditingController editor2 = TextEditingController();
  TextEditingController editor3 = TextEditingController();
  static List<String> em = [];
  List<Widget> errors = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: SingleChildScrollView(
        child: Stack(
          children: [
            Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                const SizedBox(height: 50),
                const Icon(Icons.person_add, size: 120),
                const SizedBox(height: 50),
                MyText(c: editor1, h: "Enter a username", o: false),
                const SizedBox(height: 20),
                MyText(c: editor2, h: "Enter a password", o: true),
                const SizedBox(height: 20),
                MyText(c: editor3, h: "Confirm password", o: true),
                const SizedBox(height: 20),
                SizedBox(
                  width: 180,
                  height: 60,
                  child: MySubmit(
                    ht: "btn7",
                    newFunc: () {
                      updateErrors();
                      if (errors.isEmpty) {
                        FirebaseAuth.instance.createUserWithEmailAndPassword(
                            email: editor1.text, password: editor2.text);
                        Navigator.pushReplacementNamed(context, '/login');
                      }
                    },
                  ),
                ),
              ],
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: errors,
              ),
            )
          ],
        ),
      ),
    );
  }

  void updateErrors() {
    errors = [];
    em = [];
    print("");
    print("NEW UPDATE ERROR----------------------------");

    if (editor2.text != editor3.text) {
      em.add("Confirmed password must match");
    }
    if (editor1.text.isEmpty) {
      em.add("The username field cannot be empty");
    }
    if (editor2.text.isEmpty) {
      em.add("The password field cannot be empty");
    }
    if (editor1.text.length < 8) {
      em.add("The username must be at least 8 letters");
    }

    print(em);

    for (int i = 0; i < em.length; i++) {
      errors.add(
          MyErrorBox(whichError: i, listErrors: em, key: ValueKey("errors$i")));
    }

    setState(() {});

    print("");
  }
}

Here is the output from the implemented code after running the 1st test case of "p" as editor1.text, and nothing in the other 2 textFields, and the 2nd test case of every textField empty:

NEW UPDATE ERROR---------------------------- [The password field cannot be empty, The username must be at least 8 letters]

INSIDE THE INIT

THIS IS AN ERROR BOX: The password field cannot be empty

INSIDE THE INIT

THIS IS AN ERROR BOX: The username must be at least 8 letters


As you can see, the second output does not show the 1st and 2nd error boxes in the list be initalized and they stay the same as before.

NEW UPDATE ERROR---------------------------- [The username field cannot be empty, The password field cannot be empty, The username must be at least 8 letters]

THIS IS AN ERROR BOX: The password field cannot be empty

THIS IS AN ERROR BOX: The username must be at least 8 letters

INSIDE THE INIT

THIS IS AN ERROR BOX: The username must be at least 8 letters

1
EdwynZN On

I see that you're trying to update the state by using static fields, which can give you weird results because what setState does is update the objects inside the class that extends State<T> (in your case _CreateAccountWidgetState), you should try to pass your list from the StatefulWidget to the State and then modify it while using setState so your UI rebuilds. you should pass the list to the error widgets instead of using static for all of them to avoid future problems.

Now for the problem I believe is because you're using possibleErrs to add the erors to the list, and because all of the items in possibleErrs are widgets with a value and key Flutter optimizes knowing that its the same object than before, only updating the classes that changes.

Flutter optimizes building widgets if they are the same type and the same key (which in your project it's happening) and also the same properties (whichError remains the same). So the best solution to both problems is to pass the list to those widgets instead of having a static list somewhere else

...
/// your State<>
void updateErrors() {
    errors = [];
    em = [];
    print("");
    print("NEW UPDATE ERROR----------------------------");

    if (editor2.text != editor3.text) {
      em.add("Confirmed password must match");
    }
    if (editor1.text.isEmpty) {
      em.add("The username field cannot be empty");
    }
    if (editor2.text.isEmpty) {
      em.add("The password field cannot be empty");
    }
    if (editor1.text.length < 8) {
      em.add("The username must be at least 8 letters");
    }

    for (int i = 0; i < em.length; i++) {
      errors.add(
        MyErrorBox(///Pass the list here
          whichError: 0,
          errors: em,
          key: ValueKey("error$i"),
        ),
      );
    }

    setState(() {
      CreateAccountWidget.updateErrorM(em);
    });

    print("");
  }
}
/// End of your State

class MyErrorBox extends StatefulWidget {
  final int whichError;
  final List<String> errors;

  MyErrorBox({super.key, required this.whichError, required this.errors});

  @override
  State<MyErrorBox> createState() => MyErrorBoxState();
}

class MyErrorBoxState extends State<MyErrorBox> {
  late final String errorMessage;

  @override
  //updates the variable et
  void initState() {
    // TODO: implement initState
    super.initState();
    errorMessage = widget.errors[widget.whichError];
  }

  @override
  Widget build(BuildContext context) {
    someL = CreateAccountWidget.errorMessages;
    print("IN THE BUILD METHOD:");
    print(someL[et]);
    print("");
    return SizedBox(
      width: 280,
      height: 55,
      child: DecoratedBox(
        decoration: const BoxDecoration(color: Color.fromRGBO(255, 0, 0, .7)),
        child: Center(
          child: Text(
            someL[et],
            style: const TextStyle(
              color: Colors.white,
            ),
          ),
        ),
      ),
    );
  }
}

And for even greater optimization your ErrorBox could use only the text and your State<CreateAccountWidget> should do the parsing

   for (int i = 0; i < em.length; i++) {
      errors.add(
        MyErrorBox(
          errorText: em[i],
          key: ValueKey("error$i"),
        ),
      );
    }
....
class MyErrorBox extends StatefulWidget {
  final String errorText;

  MyErrorBox({super.key, required this.errorText});

  @override
  State<MyErrorBox> createState() => MyErrorBoxState();
}