Flutter Lifecycle: State Object Lifecycle [Part 2]

Jessica Jimantoro
6 min readMar 22, 2023

--

Hey guys!
You’ve got into Part 2, thanks for reading Part 1!

Photo by Tolga Ulkan on Unsplash

Now, we’ll dive into the main topic!

createState

The createState creates the mutable state for the widget at a given location in the tree.

abstract class StatefulWidget extends Widget {
...
@override
StatefulElement createElement() => StatefulElement(this);

@protected
@factory
State createState();
}
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
...
}
}

You can see that the StatefulWidget creates an Element, StatefulElement. The createState is called in StatefulElement constructor’s initializer (widget.createState()).

And just FYI, the StatefulElement extends the ComponentElement that is used to encapsulate the render object called. Instead of calling the render object directly, Flutter creates a ComponentElement that will create the render object by making other elements.

initState

The initState is called when the widget is inserted into the tree.

abstract class State<T extends StatefulWidget> with Diagnosticable {
...
@protected
@mustCallSuper
void initState() {}
...
}
class StatefulElement extends ComponentElement {
...
@override
void _firstBuild() {
final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
state.didChangeDependencies();
super._firstBuild();
}
...
}

It only gets called when first build and only gets called once (state.initState()). When does the _firstBuild get called? It’s when it gets mounted by connected with its BuildContext.

In this article, we don’t talk so much about the mount, it will be talked on the next part. What you need to know is the initState gets called when the widget gets mounted, and mounted happened after the widget has been created. That’s why if you do print(mounted) on initState, the result will always be true.

didChangeDependencies

The didChangeDependencies is called when a dependency of this State object changes.

abstract class State<T extends StatefulWidget> with Diagnosticable {
...
@protected
@mustCallSuper
void didChangeDependencies() { }
...
}
class StatefulElement extends ComponentElement {
@override
void _firstBuild() {
...
final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
state.didChangeDependencies();
...
super._firstBuild();
}

@override
void performRebuild() {
if (_didChangeDependencies) {
state.didChangeDependencies();
_didChangeDependencies = false;
}
super.performRebuild();
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
_didChangeDependencies = true;
}
}

It gets called right after the initState, that’s why didChangeDependencies immediately get called after the initState (state.didChangeDependencies) and it is also called when the elements need to rebuild if the dependencies changed.

It is safe to call the BuildContext.dependOnInheritedWidgetOfExactTypeon this method. The dependOnInheritedWidgetOfExactType is typically called implicitly from of() static methods to call the dependency itself.

For example, MediaQuery. It’s an InheritedWidget and when you want to get the size, you usually called it like MediaQuery.of(context).size. When you call the of method, it will be called the dependOnInheritedWidgetOfExactType. You cannot call this method inside initState or dispose, because they’re not in active phase, but you can call the method inside didChangeDependencies, didUpdateWidget, or build method.

When to call .of(context) in didChangeDependencies? Call it when you need to do an expensive calculation. If it’s just a simple thing, you can put it on the build method immediately.

It’s also possible to put API fetching / DB reading inside the didChangeDependencies, but I rarely saw this practice. They prefer to use the State Management like Provider, BLoC, etc. You can see that this method gets called when the dependencies got changed and need to be rebuilt. The State Management used is purposed to prevent calling the build method frequently if you have so many widgets or just to separate the business logic & view.

build

The build method describes the part of the user interface represented by this widget.

class StatefulElement extends ComponentElement {
@override
Widget build() => state.build(this);
}
abstract class ComponentElement extends Element {
void _firstBuild() {
rebuild();
}
@override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
Widget? built;
...
built = build();
super.performRebuild();
}
}
abstract class Element extends DiagnosticableTree implements BuildContext {
void rebuild({bool force = false}) {
performRebuild();
}
@protected
@mustCallSuper
void performRebuild() {
_dirty = false;
}
}

In this method, you can just write what widget you want to build i.e. ‘Hello World’ Text with Center layout.

From the code, you can see the state.build gets called inside StatefulElement’s build method. The build is called when the first time & whenever it needs to be rebuilt. Then, the element will be flagged as clean (_dirty = false).

didUpdateWidget

The didUpdateWidget method is called whenever the widget configuration changes.

class StatefulElement extends ComponentElement {
@override
void update(StatefulWidget newWidget) {
super.update(newWidget);
final StatefulWidget oldWidget = state._widget!;
state._widget = widget as StatefulWidget;
final Object? _ = state.didUpdateWidget(oldWidget) as dynamic;
rebuild(force: true);
}
}
abstract class Element extends DiagnosticableTree implements BuildContext {
@mustCallSuper
void update(covariant Widget newWidget) {
_widget = newWidget;
}

void rebuild({bool force = false}) {
performRebuild();
}

@protected
@mustCallSuper
void performRebuild() {
_dirty = false;
}
}

From this code, you can see that didUpdateWidget is called when update whereas this method is called when the parent wants to change the configuration. For example, you have a StatefulWidget class that has a title property with a ‘Hello’ value, and the parent changes the title into a ‘World’ value. At this moment, the configuration is changed and didUpdateWidget will get called.

You also can see that it is forced to rebuild when didUpdateWidget gets called. So, putting setState to change the configuration here is redundant.

setState

The setState method notifies the framework that the internal state of this object has changed.

abstract class State<T extends StatefulWidget> with Diagnosticable {
@protected
void setState(VoidCallback fn) {
_element!.markNeedsBuild();
}

abstract class Element extends DiagnosticableTree implements BuildContext {
void markNeedsBuild() {
if (dirty) {
return;
}
_dirty = true;
owner!.scheduleBuildFor(this);
}
}

The setState is used to trigger rebuild, that’s why it calls the markNeedsBuild. So, it’s just a method to schedule the build. How about if you don’t set your state inside the setState’s callback parameter (VoidCallback fn)? Let’s see the example below:


class MyHomePage extends StatefulWidget {
...
}

class _MyHomePageState extends State<MyHomePage> {
String _title = 'Hello World!';

@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_title),
ElevatedButton(
child: const Text('Change Title'),
onPressed: () {
_title = 'Hi!!';
setState(() {});
},
),
],
),
);
}
}

It still gets updated to “Hi!!” after you click the button. Once again, the cause is just a method to do rebuild. But, just do state changes inside the callback function, because it contains important validations i.e. you can’t setState a Future.

When the build method gets called, the whole build method gets rebuilt. That’s why you should be careful to use this method. For example, if you have a simple form with submit button that can be enabled or disabled, you can just use the setState to change the submit button view. With a simple form, you don’t need to separate any components into widgets. But, in some cases when you get multiple fields, you’ll need to create your components (text field, button, etc.) separately for sake of the better readability and the easiest to manage the state, not because of performance reasons.

However, another example like displaying the ListView with a large number of items, you’ll need to make your item widget instead of using the helper method for the sake of performance. If you have a bunch of expensive widgets, it also needs to be separated.

dispose

The dispose method is called when this object is removed from the tree permanently.

abstract class StatefulElement extends ComponentElement {
@override
void unmount() {
super.unmount();
state.dispose();
state._element = null;
_state = null;
}
}

It gets called on unmount, a process of removing a widget from the widget tree and disposing of its resources (state.dispose()). It becomes the end of the lifecycles.

At this moment, you should dispose or cancel your controller, subscription, etc. here to prevent the memory leak.

Yeah, thank you for taking time to read this article. In the next part, I will write about the mount, unmount, activate, and deactivate.

Feel free to leave a comment below or correct me if there’s anything wrong, and let’s get connected on LinkedIn!

Thank you and see you! 🐈

--

--

Jessica Jimantoro
Jessica Jimantoro

Written by Jessica Jimantoro

Software Engineer & Flutter Developer 👩‍💻, Amateur anime-style sculptor 🧸, Tarot Reader 🔮