Skip to content
Boring Doc
Boring Doc explanation Verified against Flutter 3.47.7 3 min readwidgetsStatelessWidgetStatefulWidgetStateElement

Widgets and the widget lifecycle

Everything is a widget: build, rebuild, dispose.

A widget is an immutable description of part of the UI. Flutter turns the widget tree into a longer-lived element tree and a render tree; when you call setState, only the widgets that describe the changed subtree are rebuilt and the elements decide what actually needs to be updated on screen.

  1. Widgets are immutable configuration objects; a new widget instance is cheap and is created on every build.
  2. StatelessWidget: build() depends only on its constructor arguments and BuildContext.
  3. StatefulWidget creates a State object that survives rebuilds and holds mutable data.
  4. State lifecycle: initState → didChangeDependencies → build → (didUpdateWidget / setState → build)* → deactivate → dispose.
  5. Three trees: widgets (what you write), elements (the persistent instances that hold state), render objects (layout and painting).
  6. setState schedules a rebuild of that State's subtree; it does not rebuild the whole app and it must not be called after dispose.
Widget treeWidget tree → element tree → render tree
Tree

Blueprints, construction crew, and the building

A widget is a blueprint: a cheap, immutable sheet of paper that says what should exist. The element tree is the construction crew that reads blueprints and keeps track of what has already been built; when you hand the crew a new blueprint for the same spot, they compare it with the previous one and only touch what differs. The render tree is the physical building: walls (layout) and paint (rendering). State objects are the crew's notebooks: they persist between blueprint revisions, which is why a counter keeps its value when the page is rebuilt.

Where the analogy breaks: Real crews rebuild slowly; Flutter's element diffing runs every frame and discards old widgets immediately, so widgets are far cheaper than blueprints. The analogy also hides keys: without a Key, the crew matches blueprints by type and position only, so reordering stateful children can attach the wrong notebook to the wrong building.

Minimal working example

A StatefulWidget with a counter
dart
import 'package:flutter/material.dart';

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _count = 0;

  void _increment() {
    setState(() {
      _count++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Counter')),
      body: Center(child: Text('Count: $_count')),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: const Icon(Icons.add),
      ),
    );
  }
}
Expected: Tapping the button increments the number. Only CounterPage's subtree is rebuilt; MaterialApp and the Scaffold's unchanged children are not.Flutter ≥ 3.0.0StatefulWidget API docs
  1. L2Widgets are immutable; const constructors let Flutter skip rebuilding identical subtrees.
  2. L6createState is called once per element; the returned State persists across rebuilds.
  3. L10Mutable data lives in State, never in the widget.
  4. L13setState marks this element dirty; build runs again before the next frame.
  5. L19build must be pure: no side effects, no setState calls inside it.

Use cases

  • Use a StatelessWidget whenever the UI is a pure function of its inputs (most leaf and layout widgets).
  • Use a StatefulWidget for local, ephemeral state: a toggle, a text field's focus, an animation controller, a page index.
  • Use lifecycle hooks (initState/dispose) to own resources that must be created once and released: controllers, subscriptions, focus nodes.

Benefits

  • Immutable widgets make rebuilds cheap and predictable; Flutter can diff and reuse elements efficiently.
  • Local state stays close to the UI that uses it, which keeps small components self-contained.

Limitations

  • State in a StatefulWidget is lost when the element is removed from the tree (navigating away, conditional rendering, changing Keys).
  • Sharing a StatefulWidget's state with distant widgets requires lifting it up or an app-state solution; see the state management topic.
  • Calling setState in build or after dispose is an error.

Alternatives and trade-offs

Doing work in build()

Problem: build can run many times per second. Creating controllers, starting network calls or subscribing to streams inside build leaks resources and repeats work.

Fix: Create long-lived objects in initState and release them in dispose. Keep build a pure description of the UI.

✗ Controller created in build (leaks)
dart
@override
Widget build(BuildContext context) {
  final controller = TextEditingController(); // recreated every build
  return TextField(controller: controller);
}
✓ Controller owned by the State
dart
late final TextEditingController _controller;

@override
void initState() {
  super.initState();
  _controller = TextEditingController();
}

@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

@override
Widget build(BuildContext context) => TextField(controller: _controller);

Calling setState after the widget was disposed

Problem: An async callback (HTTP response, timer, stream) completes after the user navigated away, and setState throws 'setState() called after dispose()'.

Fix: Check `mounted` before calling setState in async callbacks, and cancel timers and subscriptions in dispose.

✗ Unguarded async setState
dart
Future<void> _load() async {
  final data = await fetchData();
  setState(() => _data = data); // may run after dispose
}
✓ Guarded with mounted
dart
Future<void> _load() async {
  final data = await fetchData();
  if (!mounted) return;
  setState(() => _data = data);
}

Reordering stateful children without Keys

Problem: Elements are matched by runtimeType and position. When a list of stateful items is reordered, each State stays at its old position and shows the wrong data.

Fix: Give each child a stable Key (for example ValueKey(item.id)) so the element diffing algorithm moves the State with the item.

Mutating state without setState

Problem: Changing a field directly updates the value but never schedules a rebuild, so the UI shows stale data until something else triggers a build.

Fix: Wrap every state mutation that should be visible in setState(() { ... }).

This page is Boring Doc’s independent explanation. For the complete specification use the official pages below (content © The Flutter Authors, CC BY 3.0; code samples BSD). Dates show when the explanation was last checked and when the official page last changed.

  • Flutter architectural overview Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/resources/architectural-overview
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Widget fundamentals (learning pathway) Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/learn/pathway/tutorial/widget-fundamentals
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • Stateful widgets (learning pathway) Flutter 3.47.7 Explanation verified against current source
    https://docs.flutter.dev/learn/pathway/tutorial/stateful-widget
    Last checked
    Oct 21, 2026
    Source last updated
    3 weeks ago
    Last synchronized
    3 weeks ago
  • https://api.flutter.dev/flutter/widgets/State-class.html
    Last checked
    Oct 21, 2026
Was this useful?
AI explanationAI-generated · grounded in cited sources
AI explanations are not configured
Set AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.