StatefulWidget: what it is, lifecycle, and how it works

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

StatefulWidget is a Flutter widget with mutable state, allowing the UI to react to user actions, asynchronous events, and data streams. According to the official Flutter documentation (Flutter.dev, 2026), StatefulWidget is used for all interactive application elements: input forms, animations, checkboxes, switches, and screens that load data from the network. Unlike StatelessWidget, it creates a separate State object that persists throughout its entire lifecycle and can be rebuilt without recreating the widget itself.

Key Takeaways

  • StatefulWidget is a widget that can change its state during runtime, triggering UI rebuilds via setState
  • Lifecycle — StatefulWidget goes through createState, initState, didChangeDependencies, build, didUpdateWidget, dispose stages
  • State object is a separate object that holds state and exists independently of the widget throughout its entire lifetime
  • setState is the only legitimate way to notify Flutter that the widget needs to be rebuilt after data changes
  • Performance — excessive use of StatefulWidget increases memory consumption and rendering time

What is StatefulWidget?

StatefulWidget is a Flutter class that can change its state in response to user actions, system events, or asynchronous operations. Unlike StatelessWidget, StatefulWidget is not rendered directly — it creates a State object that handles rendering. This separation into two classes (Widget and State) allows Flutter to rebuild the UI without recreating the widget itself, providing a significant performance advantage during frequent updates.

The architecture of StatefulWidget follows the “separation of mutable and immutable” pattern: the widget itself remains immutable (like StatelessWidget), while all mutable state is stored in a separate State object. This allows Flutter to reuse widgets by comparing them by type and Key, while preserving the actual state between rebuilds.

According to Google (Flutter Architectural Overview, 2026), StatefulWidget is optimal for scenarios where state changes more than once during the widget’s lifetime: text fields, animations, timers, data streams, asynchronous loads. For one-time initialization, StatelessWidget is sufficient.

When StatefulWidget is necessary

StatefulWidget is mandatory when the widget must respond to external events: button clicks, HTTP request completion, database data updates, WebSocket subscriptions. It is also necessary for widgets with animations, text fields with controllers, and components that manage focus. If a widget only displays data and does not generate events, use StatelessWidget.

Internal structure

StatefulWidget consists of two classes: the StatefulWidget itself (lightweight, immutable) and State (heavy, mutable). The framework creates State via the createState() method, called once upon insertion into the tree. State receives a reference to the widget through the widget property and can access its fields at any point in the lifecycle.

StatefulWidget Lifecycle

Lifecycle of StatefulWidget consists of six main stages, each providing an overridable method for performing specific tasks. Understanding these stages is critical for proper resource management and avoiding memory leaks.

createState

createState is the first lifecycle method, called when StatefulWidget is inserted into the tree. It must return a new State instance associated with this widget. This method is called exactly once during the entire element’s lifetime. It is important not to perform heavy operations here — createState should be as lightweight as possible.

initState

initState is called immediately after State creation, before the first UI build. Here you perform: initialization of controllers (TextEditingController, AnimationController), subscription to data streams (StreamSubscription), timer setup, and initial field initialization. According to Flutter docs (Flutter.dev, 2026), you cannot call BuildContext.of() in initState — the tree is not yet fully mounted.

didChangeDependencies

didChangeDependencies is called after initState and every time InheritedWidget dependencies change. This is a suitable place to call MediaQuery.of(context) or subscribe to Theme — values that may change during the application’s runtime. If a widget uses InheritedWidget, initialization logic should be here, not in initState.

build and didUpdateWidget

build is the main method that returns the widget tree. It is called after initState, after didChangeDependencies, and after each setState. didUpdateWidget is called when the parent rebuilds and passes a StatefulWidget with new parameters. Here you can compare old and new widget fields and, if necessary, update the state.

dispose

dispose is the final stage of the lifecycle. All resources are released here: streams are unsubscribed, controllers are disposed, timers are canceled. Not calling dispose leads to memory leaks. After dispose, State is considered dead — calling setState inside it throws an exception.

How does StatefulWidget work?

The working mechanism of StatefulWidget is based on the coordinated work of three entities: Widget (lightweight description), Element (intermediate layer), and State (data storage). When Flutter encounters a StatefulWidget in the description, it creates a StatefulElement, which calls createState and stores a reference to the State object. When the parent rebuilds, Flutter compares the new widget with the current Element — if the type and Key match, the Element is updated, and the State remains the same.

State is only changed through the setState call, which notifies the framework that a rebuild is needed. It is important to understand: setState does not automatically change the state — it only marks the widget as “dirty”. The developer independently updates the State fields in the callback passed to setState. After the callback completes, Flutter calls build and updates the UI.

According to the Dart/Flutter team (Dart Language Specification, 2026), this separation ensures that all state changes occur synchronously before build is called, eliminating the situation where the UI displays partially updated data. This is a key interface consistency mechanism in Flutter.

Dart Code Examples

Let’s look at a simple StatefulWidget — a button click counter. It demonstrates the basic pattern: creating State, initializing a field in initState, changing via setState:

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

  @override
  State<CounterScreen> createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $_count'),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('Increment'),
        ),
      ],
    );
  }
}

An example with asynchronous data loading and lifecycle management. A StatefulWidget loads data from the network and displays the loading state:

dart
class UserProfilePage extends StatefulWidget {
  final String userId;
  const UserProfilePage({super.key, required this.userId});

  @override
  State<UserProfilePage> createState() => _UserProfilePageState();
}

class _UserProfilePageState extends State<UserProfilePage> {
  UserModel? _user;
  bool _isLoading = true;

  @override
  void initState() {
    super.initState();
    _loadUser();
  }

  Future<void> _loadUser() async {
    final user = await UserService.fetchUser(widget.userId);
    setState(() {
      _user = user;
      _isLoading = false;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) return const CircularProgressIndicator();
    return Text('Hello, ${_user!.name}');
  }
}

In the second example, it is important to note: initState starts an asynchronous operation, but the method itself is not asynchronous. Asynchrony is implemented via async/await inside a separate method _loadUser, which updates the state via setState after the request completes. This approach ensures that the widget correctly displays the loading indicator before data is received.

StatefulWidget vs StatelessWidget

The choice between StatefulWidget and StatelessWidget is not only about having state. StatefulWidget provides a full lifecycle with initState, didChangeDependencies, didUpdateWidget, and dispose methods, which are necessary for working with controllers, animations, and streams. StatelessWidget, on the other hand, does not have these methods and is always lighter for the framework.

The Flutter team recommendation (Flutter docs, 2026) is to minimize the number of StatefulWidgets in an application by lifting state up the tree (State Hoisting) or using state management solutions (Riverpod, Bloc, Provider). Each StatefulWidget creates a State object that lives until the element is removed — the more such widgets, the higher the memory load.

CriterionStatefulWidgetStatelessWidget
StateMutableImmutable
Lifecycle6 stagesbuild only
State objectCreated separatelyNot required
setStateAvailableNot available
SubscriptionsinitState/disposeNot supported
const constructorLimitedFully supported
Memory consumptionHigherLower

Performance and Optimization

StatefulWidget requires more resources than StatelessWidget due to the need to create and maintain a State object. However, proper use of StatefulWidget does not lead to performance problems if a few rules are followed. First, avoid deep nesting of StatefulWidget — each level adds overhead to tree traversal. Second, break complex StatefulWidget into several simple ones, each responsible for its own part of the state.

According to Flutter Performance research (Flutter.dev, February 2026), the most common cause of FPS drops is calling setState in a parent widget that rebuilds all descendants, including StatelessWidgets that have not changed their display. The solution is to extract the mutable part of the UI into a separate StatefulWidget so that setState only rebuilds the minimum necessary widgets.

Using const inside State is another important technique. If child widgets are declared as const, Flutter will not rebuild them when setState is called in the parent. This reduces the load on the framework and decreases frame rendering time.

Avoid frequent setState

Each setState call triggers a full widget rebuild. If state changes at high frequency (e.g., animation or data stream), consider using AnimatedBuilder, ValueListenableBuilder, or StreamBuilder instead of manually calling setState. These widgets optimize rebuilding, updating only the part of the UI that has actually changed.

Common Mistakes

The first common mistake with StatefulWidget is calling setState after dispose. When a widget is removed from the tree, State is considered dead, and any setState call throws a “setState called after dispose” exception. This most often happens when an asynchronous operation completes after the widget has been removed. The solution is to check the mounted flag before calling setState or cancel async operations in dispose.

The second mistake is performing heavy computations in the build method. Since build is called on every setState and on every parent rebuild, all computations should be as lightweight as possible. If a resource-intensive operation is needed, move it to a separate Isolate or cache the result in a State field.

The third mistake is not calling super.initState() and super.dispose(). When overriding these methods, the developer must call the parent implementation. Failing to do so prevents the framework from properly managing the Element state, leading to hard-to-track bugs.

Recommendations for avoiding mistakes

  • Always check mounted before setState in async callbacks
  • Don’t forget to call super.initState() and super.dispose()
  • Don’t make HTTP requests directly in build — use initState
  • Unsubscribe from all subscriptions in dispose
  • Use a minimal number of StatefulWidgets in your project

Frequently Asked Questions

How is StatefulWidget different from StatelessWidget?

StatefulWidget can change its state via setState, has a lifecycle (initState, dispose) and creates a separate State object. StatelessWidget cannot change state and has no lifecycle methods — it simply displays the passed data.

How many times is createState called?

createState is called exactly once for each StatefulElement instance. Even if the parent rebuilds multiple times, as long as the widget type and Key do not change, createState is not called — the existing State object is used.

What happens if dispose is not called?

Resources will not be released: controllers will continue running in the background, stream subscriptions will remain active, timers will not be canceled. This leads to memory leaks and can cause setState calls after dispose, which throws an exception.

Can StatefulWidget be const?

Yes, the constructor of StatefulWidget can be const. However, this does not provide the same benefit as for StatelessWidget — the State object will still be created on first insertion. const only affects the widget itself (the lightweight wrapper), not the State.

Why is the didUpdateWidget method needed?

didUpdateWidget is called when the parent passes a StatefulWidget with new parameters. This is needed to synchronize the state with new data — for example, if userId in the parameters has changed, the new user’s profile needs to be loaded.

Summary

  • StatefulWidget is a widget with mutable state, using a separate State object to store data and manage the lifecycle
  • Lifecycle consists of createState, initState, didChangeDependencies, build, didUpdateWidget, and dispose, each with its own purpose
  • setState is the only legitimate way to notify the framework of a state change, after which build is automatically called
  • mounted is a flag that must be checked before calling setState in async operations to avoid an exception after dispose
  • Performance — StatefulWidget requires more resources than StatelessWidget; it is recommended to minimize their number by lifting state to external layers
  • const child widgets inside State help reduce the amount of rebuilding when setState is called, improving performance
  • Correct choice — use StatefulWidget only when the widget needs to manage mutable data or asynchronous operations

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