Search code examples
flutterdartsetstate

Why doesn't an infinite loop occur when calling setState inside the build function?


I don't understand, why calling setState inside build doesn't occur infinite loop.

For example:

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    callSetState();
    return Container();
  }

  void callSetState() {
    setState(() {});
  }
}

Its really weird for me. Could anyone explain me please?


Solution

  • I made small research and finally figured out why doesn't an infinite loop occur.

    Starting from the beginning

    As we remember, a widget is merely a configuration that defines an Element. Widget and Element always exist together. An Element holds a reference to the widget that created it, references to its children, parent, RenderObject, and also to the State in the case of StatefulWidget, but we'll get to that later. All elements are arranged in a tree, establishing the structure of relationships.

    The Element implements the BuildContext interface, providing a safe interaction with the element through a limited set of getters and methods.

    So, what happens: Our widget creates a StatefulElement through the createElement() method. During the element creation, the widget.createState() method will be called in the constructor, which creates the State and associates it with the element (with BuildContext). Next, the mount() method will be called, which mounts the element into the element tree. It's at this point that the mounted property becomes true. If we recall the second answer, it becomes clear why it's incorrect; the mounted property becomes true before we even reach build(). Also, inside the mount() method, the _firstBuild() method will be called, and a chain of calls will lead us to our build() method in State.

    How does setState() work

    If we open the code of the method in the sources, here's what we'll see:

    void setState(VoidCallback fn) {
        final Object? result = fn() as dynamic;
        _element!.markNeedsBuild();
    }
    

    That is, setState() simply calls the markNeedsBuild() method on the element, after first calling the callback fn(). But why do we need this wrapper?

    One of the members of the Flutter team provided an answer to that. The essence is that before the introduction of setState(), developers often called markNeedsBuild() just in case, which naturally affected performance. A more meaningful name resolved this issue.

    Moreover, even with an empty callback, the widget will still be rebuilt. However, it is recommended to include in the callback only those changes that are the actual reason for rebuilding.

    The markNeedsBuild() method

    Let's see what happens inside this method:

    void markNeedsBuild() {
        if (_lifecycleState != _ElementLifecycle.active) {
          return;
        }
        if (dirty) {
          return;
        }
        _dirty = true;
        owner!.scheduleBuildFor(this);
    }
    

    Let's start from the end. In this case, owner is an object of the BuildOwner class that manages the lifecycle of the element. Through it, we mark our element as "dirty," meaning it requires rebuilding. After that, it's placed in the list of "dirty" elements that will be rebuilt on the next frame. If it's already dirty, scheduleBuildFor() won't be called again.

    So why doesn't it cause an infinite loop

    It's simple, the answer lies in the source code:

    /// Returns true if the element has been marked as needing rebuilding.
    ///
    /// The flag is true when the element is first created and after
    /// [markNeedsBuild] has been called. The flag is reset to false in the
    /// [performRebuild] implementation.
    bool get dirty => _dirty;
    bool _dirty = true;
    enter code here
    

    The _dirty parameter has a default value of true, meaning the element is marked as "dirty" from the very beginning. Therefore, during the first call to the build() method, in markNeedsBuild(), we won't get to scheduleBuildFor(), and the element won't be queued for rebuilding. There won't be an infinite loop.

    As mentioned in the comments above, after the build method is executed, the performRebuild() method resets the _dirty flag, making it ready for the next setState() call.