69

Basically I am trying to make an app whose content will be updated with an async function that takes information from a website, but when I do try to set the new state, it doesn't reload the new content. If I debug the app, it shows that the current content is the new one, but after "rebuilding" the whole widget, it doesn't show the new info.

Edit: loadData ( ) method, basically read a URL with http package, the URL contains a JSON file whose content changes every 5 minutes with new news. For example a .json file with sports real-time scoreboards whose scores are always changing, so the content should always change with new results.

class mainWidget extends StatefulWidget
{    
  State<StatefulWidget> createState() => new mainWidgetState();
}

class mainWidgetState extends State<mainWidget>
{

  List<Widget> _data;
  Timer timer;

  Widget build(BuildContext context) {
     return new ListView(
              children: _data);
  }

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

class childWidget extends StatefulWidget {
  childWidget(String s){
    _title = s;
  }

  Widget _title;

  createState() => new childState();
}

class childState extends State<gameCardS> {

  Widget _title;

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

  }

  initState()
  {
    super.initState();
    _title = widget._title;
  }
}
Jesús Martín
  • 1,293
  • 2
  • 11
  • 11
  • Please describe the various moving parts and provide example values for things that are expected to change. We need inputs and desired output. – sorak Feb 17 '18 at 21:02
  • I just added more information about the problem. – Jesús Martín Feb 17 '18 at 22:14
  • Have you tried to just wrap your _title = widget._title; in a setState? – Robert Feb 17 '18 at 22:37
  • @Robert Yes, I have to add that child Statefulwidget (the one with GestureRecognizer) works ok, the problem is trying to refresh the ancestor with new instances of childWidget – Jesús Martín Feb 17 '18 at 22:44
  • normally what I do is create a future and use Futurebuilder, that seems to do the job for me every time. You can do the same thing with Streams as well. – Robert Feb 17 '18 at 22:56

10 Answers10

36

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}';
    }
}
Simon
  • 10,932
  • 50
  • 49
  • 10
    @JesúsMartín I forgot to mention if you have multiple list items of the same type then you should give each a Key. See https://flutter.io/widgets-intro/#keys for more info. – Simon Feb 19 '18 at 13:12
  • Can someone really explain what was really the reason of the bug on his code ? i m having similar problem. – Robison Karls Custódio Jan 19 '23 at 14:43
31

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.

gregthegeek
  • 1,353
  • 3
  • 14
  • 24
  • 1
    Thanks, that worked for me. But I'm still confused why it didn't work without that.. – Emre Akcan May 18 '21 at 11:26
  • well this fixed the issue.. i turned so many stateful widgets into stateless because of this bug.... turns out all you need is this simple fix. however, now i question myself. why do i even need stateful widgets? i'm so confused... – raiton Apr 16 '23 at 19:22
25

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),
        );
    }
}
eriel marimon
  • 1,230
  • 1
  • 19
  • 29
13

I'm unsure why this happens when calling setState(...) in an async function, but one simple solution is to use:

WidgetsBinding.instance.addPostFrameCallback((_) => setState(...));

instead of just setState(...)

caneva20
  • 490
  • 6
  • 14
  • 7
    For anyone interested: By doing this, you are calling setState() AFTER the build. – Paul Oct 09 '21 at 14:57
5

The real issue on child StatefulWidget not rebuilding is in the KEY

Hey, I'm a bit late to the discussion, but I think this is important. I was facing a similar problem a while back and I even came to this thread to get some ideas.

In my case, I was simply getting widget.value directly inside the build method of the childWidget, and it was not updating when i called setState in the mainWidget.

Then i found this video: https://youtu.be/kn0EOS-ZiIc (When to Use Keys - Flutter Widgets 101 Ep. 4) - Here the Google dev talks about how keys in Flutter.

The short answer is

In a StatefulWidget the actual value you pass is stored in the state, not in the widget itself, like a StatelessWidget does.

When you call setState in the mainWidget, Flutter walks down the widget tree and checks each childWidget's type and key, to see if anything has changed. As stateful widgets store their values in the state, Flutter thinks the child widgets did not change (because the types and keys are the same) and does not rebuild them, even if the value changed.

The real solution is to give the widget a key containing the value that is changing, so when Flutter is walking down the tree, it notices that the key changed, and rebuilds the stateful widget.

Other solutions here may work as well, but if you want to really understand it, this video is worth watching.

mavini.s
  • 103
  • 1
  • 6
  • 1
    thank you, this made me really understand what causes it instead of just fixing it –  Mar 27 '23 at 12:36
4

This fixed my issue... If you have an initial value to be assigned on a variable use it in initState()

Note : Faced this issue when I tried to set initial value inside build function.

@override
  void initState() {
    count = widget.initialValue.length; // Initial value
    super.initState();
  }
Abhin Krishna KA
  • 745
  • 8
  • 13
3

don't use a future within a future; use different function that will return each future individually like this

 List<Requests> requestsData;
 List<DocumentSnapshot> requestsDocumentData;
 var docId;



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

    getRequestDocs();
  }

  Future<FirebaseUser> getData() {
    var _auth = FirebaseAuth.instance;
    return _auth.currentUser();
  }

  getRequestDocs() {
    getData().then((FirebaseUser user) {
      this.setState(() {
        docId = user.uid;
      });
    });

    FireDb()
        .getDocuments("vendorsrequests")
        .then((List<DocumentSnapshot> documentSnapshots) {
      this.setState(() {
        requestsDocumentData = documentSnapshots;
      });
    });

    for (DocumentSnapshot request in requestsDocumentData) {
      this.setState(() {
        requestsData.add(Requests(
            request.documentID,
            request.data['requests'],
            Icons.data_usage,
            request.data['requests'][0],
            "location",
            "payMessage",
            "budget",
            "tokensRequired",
            "date"));
      });
    }
  }

you can create individual functions for

  FireDb().getDocuments("vendorsrequests")
            .then((List<DocumentSnapshot> documentSnapshots) {
          this.setState(() {
            requestsDocumentData = documentSnapshots;
          });
        });

and

  for (DocumentSnapshot request in requestsDocumentData) {
          this.setState(() {
            requestsData.add(Requests(
                request.documentID,
                request.data['requests'],
                Icons.data_usage,
                request.data['requests'][0],
                "location",
                "payMessage",
                "budget",
                "tokensRequired",
                "date"));
          });
        }

I found that the use of

this

with setState is must

Rashid Iqbal
  • 1,123
  • 13
  • 13
0

first check whether it is a stateless or stateful widget,and if the class is stateless then make it to a stateful widget and try adding a code after closing the setState(() { _myState = newValue; });

Ke1212
  • 89
  • 9
0

In my case, it was just defining the state as a class property and not a local variable in the build method

Doing this -

  List<Task> tasks = [
    Task('Buy milk'),
    Task('Buy eggs'),
    Task('Buy bread'),
  ];
  @override
  Widget build(BuildContext context) {
  
    return ListView.builder(
      itemBuilder: (context, index) => TaskTile(
...

instead of this -

 @override
  Widget build(BuildContext context) {
  List<Task> tasks = [
    Task('Buy milk'),
    Task('Buy eggs'),
    Task('Buy bread'),
  ];
  
    return ListView.builder(
      itemBuilder: (context, index) => TaskTile(
...
Piyush
  • 693
  • 1
  • 7
  • 12
-3

Found the best solution. If you are using a stateless widget you can't use set state, so just convert the stateless widget to statefull widget