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 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.
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.
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 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 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 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 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 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.
| Method | When Called | Mandatory super |
|---|---|---|
| initState | When State is created | Yes, in the first line |
| didChangeDependencies | After initState and when InheritedWidget changes | Yes |
| build | After initState, didChangeDependencies, setState | No |
| didUpdateWidget | When a new widget from parent arrives | Yes |
| setState | On developer call | No |
| dispose | When removed from the tree | Yes, in the last line |
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.
A basic State example with a field modified by a timer. Demonstrates initState, setState and dispose:
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:
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 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.
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.
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.
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.
A safety pattern for asynchronous operations in State:
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
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.
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.
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.
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.
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
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.
Read also