Flutter: setState() does not trigger a build

2.6k Views Asked by At

I have a very simple (stateful) widget that contains a Text widget that displays the length of a list which is a member variable of the widget's state.

Inside the initState() method, I override the list variable (formerly being null) with a list that has four elements using setState(). However, the Text widget still shows "0".

The prints I added imply that a rebuild of the widget has not been triggered although my perception was that this is the sole purpose of the setState() method.

Here ist the code:

import 'package:flutter/material.dart';

class Scan extends StatefulWidget {
  @override
  _ScanState createState() => _ScanState();
}

class _ScanState extends State<Scan> {
  List<int> numbers;

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

  @override
  Widget build(BuildContext context) {
    print('Build was scheduled');
    return Center(
      child: Text(
        numbers == null ? '0' : numbers.length.toString()
      )
    );
  }

  Future<List<int>> _getAsyncNumberList() {
    return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
  }

  _initializeController() async {
    List<int> newNumbersList = await _getAsyncNumberList();

    print("Number list was updated to list of length ${newNumbersList.length}");

    setState(() {
      numbers = newNumbersList;
    });
  }
}

My question: why does the widget only build once? I would have expected to have at least two builds, the second one being triggered by the execution of setState().

3

There are 3 best solutions below

0
On BEST ANSWER

I have the feeling, the answers don't address my question. My question was why the widget only builds once and why setState() does not trigger a second build.

Answers like "use a FutureBuilder" are not helpful since they completely bypass the question about setState(). So no matter how late the async function finishes, triggering a rebuild should update the UI with the new list when setState() is executed.

Also, the async function does not finish too early (before build has finished). I made sure it does not by trying WidgetsBinding.instance.addPostFrameCallback which changed: nothing.

I figured out that the problem was somewhere else. In my main() function the first two lines were:

  SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
  SystemChrome.setPreferredOrientations(
    [DeviceOrientation.portraitUp,DeviceOrientation.portraitDown]
  );

which somehow affected the build order. But only on my Huawei P20 Lite, on no other of my test devices, not in the emulator and not on Dartpad.

So conclusion: Code is fine. My understanding of setState() is also fine. I haven't provided enough context for you to reproduce the error. And my solution was to make the first two lines in the main() function async:

void main() async {
  await SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);
  await SystemChrome.setPreferredOrientations(
    [DeviceOrientation.portraitUp,DeviceOrientation.portraitDown]
  );
  ...
}
6
On

Proof it works

I don't know why you say your code is not working, but here you can see that even the prints perform as they should. Your example might be oversimplified. If you add a delay to that Future (which is a real case scenario, cause fetching data and waiting for it does take a few seconds sometimes), then the code does indeed display 0.

The reason why your code works right now is that the Future returns the list instantly before the build method starts rendering Widgets. That's why the first thing that shows up on the screen is 4.

If you add that .delayed() to the Future, then it does indeed stop working, because the list of numbers is retrieved after some time and the build renders before the numbers are updated.

Problem explanation

SetState in your code is not called properly. You either do it like this (which in this case makes no sense because you use "await", but generally it works too)

    _initializeController() async {
    setState(() {
    List<int> newNumbersList = await _getAsyncNumberList();

    print("Number list was updated to list of length ${newNumbersList.length}");

    
      numbers = newNumbersList;
    });
  }

or like this

    _initializeController() async {
    List<int> newNumbersList = await _getAsyncNumberList();

    print("Number list was updated to list of length ${newNumbersList.length}");

    
    numbers = newNumbersList;
    setState(() {
    /// this thing right here is an entire function. You MUST HAVE THE AWAIT in 
    /// the same function as the update, otherwise, the await is callledn, and on 
    /// another thread, the other functions are executed. In your case, this one 
    /// too. This one finishes early and updates nothing, and the await finishes later.
    });
  }

Suggested solution

This will display 0 while waiting 5 seconds for the Future to return the new list with the data and then it will display 4. If you want to display something else while waiting for the data, please use a FutureBuilder Widget.

FULL CODE WITHOUT FutureBuilder:

class Scan extends StatefulWidget {
  @override
  _ScanState createState() => _ScanState();
}

class _ScanState extends State<Scan> {
  List<int> numbers;

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

  @override
  Widget build(BuildContext context) {
    print('Build was scheduled');

    return Center(
        child: Text(numbers == null ? '0' : numbers.length.toString()));
  }

  Future<List<int>> _getAsyncNumberList() {
    return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
  }

  _initializeController() async {
      List<int> newNumbersList = await _getAsyncNumberList();

      print(
          "Number list was updated to list of length ${newNumbersList.length}");
      numbers = newNumbersList;
      setState(() {});
  }
}

I strongly recommend using this version, since it displays something to the user the whole time while waiting for the data and also has a failsafe if an error comes up. Try them out and pick what is best for you, but again, I recommend this one.

FULL CODE WITH FutureBuilder:

class _ScanState extends State<Scan> {
  List<int> numbers;

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

  }

  @override
  Widget build(BuildContext context) {
    print('Build was scheduled');

    return FutureBuilder(
      future: _getAsyncNumberList(),
      builder: (BuildContext context, AsyncSnapshot<List<int>> snapshot) {
        switch (snapshot.connectionState) {
          case ConnectionState.waiting: return Center(child: Text('Fetching numbers...'));
          default:
            if (snapshot.hasError)
              return Center(child: Text('Error: ${snapshot.error}'));
            else
              /// snapshot.data is the result that the async function returns
              return Center(child: Text('Result: ${snapshot.data.length}'));
        }
      },
    );
  }

  Future<List<int>> _getAsyncNumberList() {
    return Future.delayed(Duration(seconds: 5), () => [1, 2, 3, 4]);
  }
}

Here is a more detailed example with a full explanation of how FutureBuilder works. Take some time and carefully read through it. It's a very powerful thing Flutter offers.

0
On

This answer may not fit the explicit Problem OP was facing, but it fits the Title of the Question.

So basically i was using a Scaffold with BottomNavigation bar. In the body i put a List<Widget> which i prepared in the initState-Function and then indexed via _bodyWidgets.elementAt(_bottomNavIndex)

Everything worked so fine, until i was changing the state of said Widgets, because it never triggered the build-Function and thus never actually updating any deeper state. I didnt use initState in a async-Function or anything like that.

What finally did it for me was, to not put the List<Widget> in a variable and define it in setState, but rather to write it directly in the body parameter of Scaffold (i guess i could also just redefine the variable in the parent build-function)

dont:

initState(){
    _bodyWidgets = [MyWidget(val: _someState),...]
}

...

build(BuildContext context){
    return Scaffold(
    body: _bodyWidgets.elementAt(_navIndex);
    ...
    );
}

do:

build(BuildContext context){
    return Scaffold(
    body: [MyWidget(val: _someState),...].elementAt(_navIndex);
    ...
    );
}