Search code examples
flutterdartproviderstatefulwidget

Flutter StatefulWidget: How to Rebuild Widget When State is Updated Externally


I'm encountering an issue with updating a variable within a StatefulWidget in Flutter. Specifically, I have a custom widget CustomErrorFormField which extends StatefulWidget. This widget contains a method setErrorList to update a list variable errorList. However, when setErrorList is called from another class, the build method in CustomErrorFormField does not reflect the updated errorList.

I understand that calling setState should trigger a rebuild of the widget, but I am unsure how to appropriately invoke setState in the StatefulWidget when the variable is updated from an external class. Here's the relevant code snippet for better understanding:

class CustomErrorFormField extends StatefulWidget {
  // Variable declaration
  List<String> errorList = [];

  // Method to update errorList
  void setErrorList(List<String> listOfError) {
    errorList = listOfError;
  }

  @override
  _CustomErrorFormFieldState createState() => _CustomErrorFormFieldState();
}

class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
  @override
  Widget build(BuildContext context) {
    // Trying to print updated errorList, but not reflecting changes
    print(widget.errorList); 
    return ... // UI Code
  }
}

In another class, I'm updating the errorList like this:

// Instance creation and update
CustomErrorFormField nameTextFild = CustomErrorFormField(...);

// Inside some method
setState(() {
  // Updating errorList using setErrorList method
  if (condition) {
    nameTextFild.setErrorList([...]);
  } else {
    nameTextFild.setErrorList([...]);
  }
});

Solution

  • It's not recommended that you change the state of a widget from outside the widget.

    What you should do instead is pass the validation logic as a function and let the widget handle the state change.

    CustomFormField:

    import 'package:flutter/material.dart';
    
    class CustomErrorFormField extends StatefulWidget {
      //Take the validation logic as a parameter.
      final List<String> Function(String value) validator;
      const CustomErrorFormField({required this.validator});
    
      @override
      _CustomErrorFormFieldState createState() {
        return _CustomErrorFormFieldState();
      }
    }
    
    class _CustomErrorFormFieldState extends State<CustomErrorFormField> {
      
      //Keep the state inside the widget itself
      List<String> errorList = [];
    
      //Update the state from inside the widget
      void setErrorList(List<String> listOfError) {
        setState(() {
          errorList = listOfError;
        });
      }
    
      @override
      void initState() {
        super.initState();
      }
    
      @override
      Widget build(BuildContext context) {
        return Form(
          child: TextFormField(
            validator: (String value){
               //Use the validation logic to decide the error.
               setErrorList(widget.validator(value))
              }
            }
          ),
        );
      }
    }
    

    I have used TextFormField as an example, you can use any widget that accepts a callback upon change.

    If you're making everything from scratch you can attach the validator function to a callback that fires when the text is changed. Usually this is done with the help of a controller.

    usage:

    final nameTextFild = CustomErrorFormField(
      key: ValueKey(count),
      labelName: "Name",
      iContext: context,
      validator: (String value) {
        if (!value.contains(RegExp(r'[0-9]'))) {
          return [];
        } else {
          return ["Invalid characters, use letters only."];
        }
      },
    );