Stateful Widget Lifecycle

Stateful Widget Lifecycle

Each of us has used Flutter's Stateful Widgets. And this is one of the must topics I always ask at interviews from candidates to demonstrate deep knowledge on the topic. In most cases, this topic makes clear the degree of the engineer. I mean, if candidate shows off good knowledge on Stateful Widgets lifecyle and how it works internally, this is a good mark and we proceed with other mid-senior topics. So now question for you, ever wondered how Stateful Widgets lifecyle works internally, what each method is used for? Let's dedicate this article on them.

What is a Stateful Widget?

To create efficient and responsive apps, understanding the Stateful Widget lifecycle s crucial. This lifecycle governs how a widget transitions through different states and how Flutter manages these changes internally. A Stateful Widget in Flutter is a widget that has mutable state. Unlike Stateless Widgets, which remain unchanged once built, Stateful Widgets can rebuild themselves based on user interaction or other events.

Examples of when to use Stateful Widgets:

  • Updating a counter when a button is pressed
  • Managing animations
  • Changing the UI based on user input

A Stateful Widget consists of two main classes:

  1. StatefulWidget: Represents the immutable widget.
  2. State: Holds the mutable state and logic for updating the UI.

Stateful Widget Lifecycle Methods

Flutter provides several lifecycle methods to help developers manage state transitions. Here’s a breakdown of each method and its role in the lifecycle:

1. constructor()

  • Purpose: Called when a Stateful Widget is first instantiated.
  • Function: Creates an instance of the Stateful Widget class.
class SomeWidget extends StatefulWidget {
    const SomeWidget({super.key});

When a Stateful Widget class is instantianed, through updateChild -> inflateWidget Flutter internally creates StatefulElement, to manage this widget's location in the tree, where Stateful Widget's createState() method is called.

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

    @override
    StatefulElement createElement() => StatefulElement(this);
class StatefulElement extends ComponentElement {
    StatefulElement(StatefulWidget widget)
        : _state = widget.createState(),
          super(widget) {
    ...

2. createState()

  • Purpose: Called before Stateful Widget is inserted into the widget tree.
  • Function: Creates an instance of the State class for the widget.
  • Typical Usage: Initializing the State object.
class SomeWidget extends StatefulWidget {
    const SomeWidget({super.key});
 
    @override
    State<SomeWidget> createState() => _SomeWidgetState();
}

class _SomeWidgetState extends State<SomeWidget> {
    ...
}

3. State is mounted

After createState() is called, State object is associated with corresponding StatefulElement (location in Element tree, but not yet inserted into it) and from that moment State object is considered to be mounted. The State object remains mounted until the framework calls dispose.

class StatefulElement extends ComponentElement {
    StatefulElement(StatefulWidget widget)
        : _state = widget.createState(),
          super(widget) {
    ...
    state._element = this;
    ...
    assert(state._debugLifecycleState == _StateLifecycle.created);
}

To note, in debug, State object has 4 states which it can be in:

enum _StateLifecycle {
  /// The [State] object has been created. [State.initState] is called at this
  /// time.
  created,

  /// The [State.initState] method has been called but the [State] object is
  /// not yet ready to build. [State.didChangeDependencies] is called at this time.
  initialized,

  /// The [State] object is ready to build and [State.dispose] has not yet been
  /// called.
  ready,

  /// The [State.dispose] method has been called and the [State] object is
  /// no longer able to build.
  defunct,
}

and initially it is in _StateLifecycle.created state.

4. initState()

  • Purpose: Called once when the State object is created.
  • Function: Used for one-time initializations like setting up listeners, fetching data, or configuring animations.
  • Important: Always call super.initState() at the start of this method.

After StatefulElement is created, mount() method is called on the element.

Element inflateWidget(Widget newWidget, Object? newSlot) {
    ...
    final Element newChild = newWidget.createElement();
    ...
    newChild.mount(this, newSlot);
    ...
}

where element is inserted into Element Tree, consequently _firstBuild() method of StatefulElement is called.

@override
void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_lifecycleState == _ElementLifecycle.active);
    _firstBuild();
    assert(_child != null);
}

with the call of _firstBuild(), State object's initState() is called, followed by State object's state gets changed to initialized state in debug mode.

@override
void _firstBuild() {
    assert(state._debugLifecycleState == _StateLifecycle.created);
    final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
    ...
    assert(() {
      state._debugLifecycleState = _StateLifecycle.initialized;
      return true;
    }());
    ...
}
@override
void initState() {
  super.initState();
  print("initState: Widget initialized");
}