Home > database >  Flutter - setState not updating the child widget variable passed as parameter
Flutter - setState not updating the child widget variable passed as parameter

Time:03-10

I'm new to flutter and trying to use two Stateful Widgets first one calling the second in build() method and I just want to update the child widget variable that is passed from parent in the constructor.

Here is the code I'm trying with.

Parent Widget

class Parent extends StatefulWidget {
  Parent({Key? key}) : super(key: key);

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

class _ParentState extends State<Parent> {
  List appointments = [];

  @override
  void initState() {
    super.initState();
    
    fetchAppointments();
  }

  @override
  Widget build(BuildContext context) {
    return Builder(builder: (BuildContext context) {
      return RefreshIndicator(
          onRefresh: () async {
            _pullRefresh();
          },
          child: ListView(
            children: [
              AppointmentsWidget(appointments: appointments)     // <----- I passed the variable in constructor and this one is updating in setState and I want it to update in the child widget too
            ],
          ),
        ),
      );
    });
  }

  _pullRefresh() {
    fetchAppointments();
  }

  fetchAppointments() {
    setState(() {
      // Stuff to do
      appointments = ......
      ......
      ......
    });
  }
}

Child Widget

class AppointmentsWidget extends StatefulWidget {
  var appointments;
  AppointmentsWidget({this.appointments});

  @override
  _AppointmentsWidgetState createState() =>
      _AppointmentsWidgetState(appointments: appointments);  // <--- Constructor 1
}

class _AppointmentsWidgetState extends State<AppointmentsWidget> {
  var appointments;
  
  _AppointmentsWidgetState({this.appointments});  // <--- Constructor 2

  @override
  Widget build(BuildContext context) {
    
    return ListView.builder(
      physics: const NeverScrollableScrollPhysics(),
      shrinkWrap: true,
      itemCount: appointments.length,
      itemBuilder: (BuildContext context, int index) {
        return Text(appointments[index].toString());     // <--- This is where I use it
      },
    );
  }
}

I know the constructor calls once but I couldn't find a way to either recall the constructor OR somehow pass the updated value to the constructor.

Your help is really appreciated.

CodePudding user response:

You should make your child widget stateless, as its state (the appointments) are handled by the parent. What happens currently is that your child widget is constructed, where the empty list is used as its widget.appointments value. Then when the appointments have been fetched, the widget.appointments rebuilds, but since the state of the child is maintained, this value is not passed on (initState of the child is not re-run).

class AppointmentsWidget extends StatelessWidget {
  final List appointments;

  const AppointmentsWidget({Key? key, required this.appointments}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      physics: const NeverScrollableScrollPhysics(),
      shrinkWrap: true,
      itemCount: appointments.length,
      itemBuilder: (BuildContext context, int index) {
        return Text(appointments[index].toString());     // <--- This is where I use it
      },
    );
  }
}

Also, take a look at the flutter docs on handling state: https://docs.flutter.dev/development/data-and-backend/state-mgmt/intro

These also state that it's good to keep your state high up (in the parent in this case), and make child widgets use the state of the parents to render themselves appropriately.

Rectification

As you mention in the comments, you need the child widget to be stateful (for maintaining state on some other data). In that case, you can simply get rid of the appointments state variable and use widget.appointments instead, which will update when the parent rebuilds with a new value.

class AppointmentsWidget extends StatefulWidget {
  var appointments;
  AppointmentsWidget({this.appointments});

  @override
  _AppointmentsWidgetState createState() =>
      _AppointmentsWidgetState();  // <--- Constructor 1
}

class _AppointmentsWidgetState extends State<AppointmentsWidget> {

  _AppointmentsWidgetState();  // <--- Constructor 2

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      physics: const NeverScrollableScrollPhysics(),
      shrinkWrap: true,
      itemCount: widget.appointments.length,
      itemBuilder: (BuildContext context, int index) {
        return Text(widget.appointments[index].toString());     // <--- This is where I use it
      },
    );
  }
}

CodePudding user response:

You need some listenable to update the widget because the context of parent and child are diferents. In that case the correct way to do it (without a state managment package) is with a InheritedWidget.

Inherited class:

class ExampleInherited extends InheritedWidget {
  final Widget child;
  final ExampleBloc exampleBloc; // <-- change notifier that can do changes and notify their children

  const ExampleInherited({Key? key, required this.child, required this.exampleBloc}) : super(key: key, child: child);

  static ExampleInherited? of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<ExampleInherited>();

  @override
  bool updateShouldNotify(covariant InheritedWidget oldWidget) => true;
}

ChangeNotifier class:

class ExampleBloc extends ChangeNotifier {

  static final ExampleBloc _exampleBloc  = ExampleBloc._internal();

  factory ExampleBloc() {
    return _exampleBloc;
  }

  ExampleBloc._internal();

  exampleMethod(){
    // here you can do whatever you need (update vars)
    notifyListeners(); // <-- this notifies the children that they need to be rebuilded
  }
}

Then set this in your parent view:

ExampleInherited(
                exampleBloc: ExampleBloc(),
                child: // content
}

And then in your child view:

@override
  Widget build(BuildContext context) {
    ExampleBloc exampleBloc = ExampleInherited.of(context)!.exampleBloc;
    return AnimatedBuilder(
        animation: exampleBloc,
        builder: (context, child) {
            return //your content
            // this will rebuild every time you call notifyListeners() in your bloc
        })
  }
  • Related