State: what it is, state management and how it works

Author: IT Sectr Published: 2026-07-01 Reading time: 9 min

State is the central data management object in Flutter, associated with StatefulWidget and responsible for storing mutable information and building the interface. According to the official Flutter documentation (Flutter.dev, 2026), State exists throughout the entire widget lifecycle and survives its rebuilds, ensuring data consistency between UI updates. Unlike the widget itself, State can modify its fields and trigger rebuilding through the setState call.

Key Takeaways

  • State — an object that stores mutable data of StatefulWidget and manages its rebuilding via setState
  • Lifecycle — State goes through initState, didChangeDependencies, build, didUpdateWidget and dispose, each stage with a clear purpose
  • mounted — a flag indicating that State is still in the widget tree and can safely call setState
  • widget — a reference to the associated StatefulWidget, accessible through the State property for reading parent parameters
  • Isolation — State is isolated from other State objects; InheritedWidget or external state management tools are used for data exchange

What is State in Flutter?

State is an object in the Flutter architecture that stores mutable data of a StatefulWidget and determines how this data is displayed in the interface. Each StatefulWidget, when inserted into the tree, creates exactly one State object via the createState method. State exists independently of the widget: if the parent rebuilds the StatefulWidget with new parameters, State remains the same and receives the updated widget through the widget property.

According to the Flutter Architectural Overview (Google, 2026), the separation of Widget and State is a deliberate architectural decision that allows the framework to reuse tree elements. The widget (a lightweight description) can be created and destroyed multiple times, but State (a heavy object with data) stays in memory as long as the element remains in the tree. This prevents data loss during frequent rebuilds of parent widgets.

State implements the StatefulWidget interface via generics: class _MyState extends State<MyWidget>. The generic binds State to a specific StatefulWidget type, providing type-safe access to its fields through the widget property.

Where is State stored?

The State object is stored in StatefulElement — an intermediate layer between Widget and RenderObject. StatefulElement creates State via createState, keeps a reference to it, and passes State as the owner. The Element is only destroyed when the widget is removed from the tree — until then, State lives in memory.

State Lifecycle

The State lifecycle is deterministic and consists of a strict sequence of calls. Understanding this sequence is the foundation for correct resource management and preventing memory leaks.

initState — initialization

initState is called first when State is created. In this method, controllers, stream subscriptions, timers and initial field values are initialized. Calling super.initState() in the first line is mandatory. At the initState stage, the widget tree is not yet fully mounted, so methods like MediaQuery.of(context) may not work correctly.

didChangeDependencies

didChangeDependencies is called after initState and whenever InheritedWidget dependencies change. This is where, not in initState, you should call MediaQuery.of(context) or Theme.of(context), because by this time the tree is already mounted. This method is also called if the widget moves to a different context where InheritedWidget provides other values.

build — building the UI

build is the main method of State that returns a widget tree. It is called after initState, after didChangeDependencies, and after each setState. The build method should have no side effects — it only describes the interface based on the current State field values.

didUpdateWidget

didUpdateWidget is called when the parent rebuilds the StatefulWidget with new parameters. State gains access to the old widget through oldWidget and can compare it with the new one. If parameters have changed, you can update the state, load new data, or restart an animation.

dispose — releasing resources

dispose is the final method where all resources are released: controllers, subscriptions, timers. After dispose, State is marked as dead: mounted returns false, calling setState throws an exception. Calling super.dispose() in the last line of the method is mandatory.

MethodWhen CalledMandatory super
initStateWhen State is createdYes, in the first line
didChangeDependenciesAfter initState and when InheritedWidget changesYes
buildAfter initState, didChangeDependencies, setStateNo
didUpdateWidgetWhen a new widget from parent arrivesYes
setStateOn developer callNo
disposeWhen removed from the treeYes, in the last line

How Does State Work?

The State working mechanism is based on three key principles: association with Element, reactivity through setState, and parent access via the widget property. When Flutter builds the element tree and encounters a StatefulElement, it calls createState of the associated widget. The created State is stored in the element and exists until the element is removed.

When setState is called, State marks itself as dirty and schedules a rebuild for the next frame. Importantly: setState does not call build immediately — it only registers the need for rebuilding. Flutter collects all dirty elements in the current frame and rebuilds them in batch, which optimizes performance. After build is called, State returns to a clean state.

The widget property allows State to read parameters passed to the StatefulWidget constructor. Since StatefulWidget is immutable (like StatelessWidget), its fields do not change — when parameters change, the parent creates a new widget, and State receives it through didUpdateWidget. This ensures that State always works with the current parent data.

Dart Code Examples

A basic State example with a field modified by a timer. Demonstrates initState, setState and dispose:

dart
class _TimerWidgetState extends State<TimerWidget> {
  int _seconds = 0;
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(
      const Duration(seconds: 1),
      (_) => setState(() => _seconds++),
    );
  }

  @override
  void dispose() {
    _timer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text('$_seconds seconds elapsed');
  }
}

An example using the widget property to access parent parameters and react to their changes via didUpdateWidget:

dart
class _GreetingState extends State<GreetingWidget> {
  String _displayName = '';

  @override
  void initState() {
    super.initState();
    _displayName = _formatName(widget.name);
  }

  @override
  void didUpdateWidget(GreetingWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.name != oldWidget.name) {
      setState(() {
        _displayName = _formatName(widget.name);
      });
    }
  }

  String _formatName(String name) => name.trim().isEmpty ? 'Guest' : name;

  @override
  Widget build(BuildContext context) {
    return Text('Hello, $_displayName!');
  }
}

In the second example, State tracks changes to the input parameter name and reformats the display only when a real change occurs. Without the check widget.name != oldWidget.name, the method would be called on every parent rebuild, even if the name did not change — unnecessary work for the framework.

State vs StatefulWidget

State and StatefulWidget are two different classes in the Flutter architecture performing different roles. StatefulWidget is a lightweight immutable wrapper that describes the widget configuration and creates State. State is a heavy object that stores mutable data, manages subscriptions, and builds the UI. This separation allows Flutter to destroy and create widgets without losing state.

All StatefulWidget fields must be final and set in the constructor — they do not change after creation. State, on the other hand, can modify its fields at any time, but all changes must be preceded by a setState call so that Flutter knows about the need for rebuilding. This is the key difference: StatefulWidget is “what to show”, State is “how to show and what data to use”.

According to Flutter source code analysis (Flutter SDK, 2026), StatefulWidget contains only one mandatory field — createState, while State has access to BuildContext, can subscribe to streams, manage animations and controllers. It is recommended to keep StatefulWidget as simple as possible, moving all logic to State.

Why can't StatefulWidget be State?

The separation of Widget and State is an architectural decision that ensures configuration immutability. If StatefulWidget itself stored state, the state would be lost on every parent rebuild. By moving state to a separate object, Flutter guarantees that data survives rebuilds, while widgets remain lightweight and comparable.

State Management Between Widgets

The State object is isolated — it does not have direct access to other widgets' State. For data exchange between widgets, InheritedWidget or external state management tools are used: Provider, Riverpod, Bloc, Redux. Each approach solves the problem differently: InheritedWidget works through the widget tree, Provider through a DI container, Bloc through event streams.

The choice of tool depends on project scale. For a small application, InheritedWidget and local State are sufficient. For medium and large projects, Riverpod or Bloc are recommended — they ensure testability, predictability, and separation of logic from UI. State is then used only for local widget data (focus, scroll, animation).

According to the Flutter Community Survey 2025 (Flutter Foundation, December 2025), Riverpod is the most popular state management solution in new projects (38%), followed by Bloc (31%) and Provider (22%). All three tools are compatible with State and do not require abandoning the standard lifecycle.

Local vs Global State

  • Local state — in the State of a specific widget (scroll position, focus state)
  • Global state — in an external store (user data, settings, cache)
  • Rule: if data is used by only one widget — store it in State
  • If data is used by 2+ widgets — move it to Riverpod/Bloc/Provider

Common Mistakes

The first mistake is forgetting to check mounted before setState in an asynchronous callback. When a widget is removed from the tree (e.g., the user navigated away), but an asynchronous operation (HTTP request) is still running, State is already dead after its completion. Calling setState in a dead State throws an exception. The check if (mounted) setState(...) solves the problem.

The second mistake is initializing InheritedWidget dependencies in initState instead of didChangeDependencies. In initState, the context is not yet mounted, so MediaQuery.of(context) will throw an exception. All InheritedWidget dependencies should be set up in didChangeDependencies or in build.

The third mistake is mutating fields without calling setState. If a developer changes a State field without setState, Flutter will not know about the change and the UI will not update. For example: _list.add(item) without a subsequent setState((){}) will modify the list, but the screen will remain the same.

Checking mounted before setState

A safety pattern for asynchronous operations in State:

dart
Future<void> _fetchData() async {
  final data = await ApiService.fetch();
  if (mounted) {
    setState(() => _data = data);
  }
}

Checking mounted ensures that setState is only called on a live State, preventing the “setState called after dispose” exception.

Frequently Asked Questions

How is State different from StatefulWidget?

StatefulWidget is an immutable widget configuration, while State is a mutable object that stores data and manages the lifecycle. Widget can be recreated, State cannot. StatefulWidget creates State via createState.

How many State objects are created for one StatefulWidget?

Exactly one. The createState method is called once when the StatefulWidget is first inserted into the tree. Even if the parent rebuilds multiple times, the State object remains the same until the widget type or Key changes.

What is mounted in State?

mounted is a boolean flag indicating whether State is in the widget tree. After dispose is called, mounted becomes false. It is used for checking before setState in asynchronous callbacks to avoid exceptions.

Can State be used without StatefulWidget?

No. State is always tied to a specific StatefulWidget via generics: State<T extends StatefulWidget>. Creating State directly, without association with a widget, is architecturally impossible.

What happens when setState is called in dispose?

An exception is thrown: “setState called after dispose”. After dispose, State is considered dead, and any attempts to rebuild the UI through setState are forbidden. The solution is to check mounted before each setState.

Summary

  • State — a StatefulWidget data management object that stores mutable fields and triggers UI rebuilding via setState
  • Lifecycle includes mandatory methods initState, didChangeDependencies, build, didUpdateWidget, and dispose, each with its own purpose
  • mounted — a critical safety flag preventing setState calls after widget removal from the tree
  • widget — a State property for accessing the associated StatefulWidget parameters, updated via didUpdateWidget
  • Isolation — State has no access to other State objects; inter-widget communication is implemented via InheritedWidget or external tools
  • setState — does not call build immediately, but only marks State as dirty for rebuilding on the next frame
  • Rule — use State for local widget data; move global state to external layers (Riverpod, Bloc)

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also