Flutter - setState not updating inner Stateful Widget

The Root issue explained

  • initState(), for the child widget, is called only once when the Widget is inserted into the tree. Because of this, your child Widget variables will never be updated when they change on the parent widget. Technically the variables for the widgets are changing, you are just not capturing that change in your state class.

  • build() is the method that gets called every time something in the Widget changes. This is the reason @gregthegeek solution works. Updating the variables inside the build method of your child widget will ensure they get the latest from parent.

Works

class ChildState extends State<ChildWidget> {
    late String _title;
    @override
    Widget build(BuildContext context) {
        _title = widget._title; // <==== IMPORTANT LINE
        return new GestureDetector(onTap: () => foo(),
            child: new Text(_title),
        );
    }
}

Does not work

(It will not update when _title changes in parent)

class ChildState extends State<ChildWidget> {
    late String _title;

    @override
    void initState() {
      super.initState();
      _title = widget._title; // <==== IMPORTANT LINE
    }

    @override
    Widget build(BuildContext context) {
        return new GestureDetector(onTap: () => foo(),
            child: new Text(_title),
        );
    }
}

This was really giving me headache and no Google results were working. What finally worked was so simple. In your child build() assign the value to the local variable before you return. Once I did this everything worked with subsequent data loads. I even took out the initState() code.

Many thanks to @Simon. Your answer somehow inspired me to try this.

In your childState:

@override
Widget build(BuildContext context) {
_title = widget._title; // <<< ADDING THIS HERE IS THE FIX
return new GestureDetector(onTap: foo(),
   child: new Card(child: new Text(_title));

}

Hopefully this works in your code. For me, I use a Map for the entire JSON record passed in, rather than a single String, but that should still work.


This should sort your problem out. Basically you always want your Widgets created in your build method hierarchy.

import 'dart:async';

import 'package:flutter/material.dart';

void main() => runApp(new MaterialApp(home: new Scaffold(body: new MainWidget())));

class MainWidget extends StatefulWidget {
    @override
    State createState() => new MainWidgetState();
}

class MainWidgetState extends State<MainWidget> {

    List<ItemData> _data = new List();
    Timer timer;

    Widget build(BuildContext context) {
        return new ListView(children: _data.map((item) => new ChildWidget(item)).toList());
    }

    @override
    void initState() {
        super.initState();
        timer = new Timer.periodic(new Duration(seconds: 2), (Timer timer) async {
            ItemData data = await loadData();
            this.setState(() {
                _data = <ItemData>[data];
            });
        });
    }


    @override
    void dispose() {
        super.dispose();
        timer.cancel();
    }

    static int testCount = 0;

    Future<ItemData> loadData() async {
        testCount++;
        return new ItemData("Testing #$testCount");
    }
}

class ChildWidget extends StatefulWidget {

    ItemData _data;

    ChildWidget(ItemData data) {
        _data = data;
    }

    @override
    State<ChildWidget> createState() => new ChildState();
}

class ChildState extends State<ChildWidget> {

    @override
    Widget build(BuildContext context) {
        return new GestureDetector(onTap: () => foo(),
            child: new Padding(
                padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 24.0),
                child: new Card(
                    child: new Container(
                        padding: const EdgeInsets.all(8.0),
                        child: new Text(widget._data.title),
                    ),
                ),
            )
        );
    }

    foo() {
        print("Card Tapped: " + widget._data.toString());
    }
}

class ItemData {
    final String title;

    ItemData(this.title);

    @override
    String toString() {
        return 'ItemData{title: $title}';
    }
}