How Does Flutter’s StatefulWidget Work? Essential Concepts to Understand Before Using Riverpod or Hooks — State, Element, the Widget Tree, and the setState Mechanism

Core Concepts Behind Flutter’s StatefulWidget

Riverpod and Flutter Hooks make state management in Flutter more convenient, but they are all built on top of the StatefulWidget system. These tools are not entirely new concepts; rather, they are abstractions and wrappers around StatefulWidget, State, Element, and the widget tree.

Because of this, understanding how StatefulWidget works internally is essential before using Riverpod or Hooks effectively.

This article breaks down Flutter’s UI system into four key components and explains how State, Element, the widget tree, and setState work together.


Flutter’s UI is composed of four cooperating components:

  • StatefulWidget (the blueprint)
  • Element (the actual UI instance and controller)
  • State (the notebook that stores data)
  • Widget tree (the Widgets returned by build)

Let’s start with a simple code example and then explore how each component works.


Code example

class MyWidget extends StatefulWidget {
  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text("count = $count"),
        ElevatedButton(
          onPressed: () {
            setState(() {
              count++;
            });
          },
          child: Text("Increase"),
        ),
      ],
    );
  }
}

The four components

1. StatefulWidget (the blueprint)

  • A simple blueprint for the UI
  • Its only responsibility is to create a State object
  • Never changes during the app’s lifetime
  • Created only once

2. Element (the actual UI instance and controller)

  • Not written in your code, but always created internally by Flutter
  • The actual UI instance and the controller
  • Manages parent–child relationships, diffing, and rendering
  • Created when the StatefulWidget is placed on the screen
  • Creation order: StatefulWidget → Element → State

3. State (the notebook storing data)

  • Created only once when Element calls createState()
  • Stores values like count
  • setState() simply notifies Element that data changed
  • The State instance itself is never recreated → only its contents change

4. Widget tree (Widgets returned by build)

return Column(
  children: [
    Text("count = $count"),
    ElevatedButton(...),
  ],
);
  • A collection of lightweight Widgets
  • Recreated every time build() is called
  • Cheap to rebuild because Widgets are immutable and lightweight

Code with print statements

class MyWidget extends StatefulWidget {
  MyWidget() {
    print("MyWidget: created");
  }

  @override
  State<MyWidget> createState() {
    print("MyWidget: createState called");
    return _MyWidgetState();
  }
}

class _MyWidgetState extends State<MyWidget> {
  int count = 0;

  _MyWidgetState() {
    print("_MyWidgetState: created");
  }

  @override
  Widget build(BuildContext context) {
    print("build: count = $count");
    return Column(
      children: [
        Text("count = $count"),
        ElevatedButton(
          onPressed: () {
            setState(() {
              count++;
              print("setState: count updated to $count");
            });
          },
          child: Text("Increase"),
        ),
      ],
    );
  }
}

What happens at startup

Explanation

  1. StatefulWidget (blueprint) is created
  2. Element (actual UI instance) is created
  3. Element calls createState() and State is created
  4. State.build() is called and the widget tree is created
  5. Element renders the UI

Console output

MyWidget: created
MyWidget: createState called
_MyWidgetState: created
build: count = 0

What happens when the button is pressed (setState)

Explanation

  1. State’s data (count) changes
  2. setState() notifies Element that data changed
  3. Element immediately calls State.build() and rebuilds the widget tree
  4. Element compares the new widget tree with the previous one
  5. RenderObject updates only the changed parts

Console output

setState: count updated to 1
build: count = 1

Why Flutter is fast (diff-based rendering)

  • The widget tree is recreated each time (lightweight, so cheap)
  • Rendering is heavy, but Element sends only the differences

Flutter redraws only what changed, making it extremely fast.

Leave a Comment

CAPTCHA