Flutter Lifecycle: State Object Lifecycle [Part 2]
Hey guys!
You’ve got into Part 2, thanks for reading Part 1!
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.dependOnInheritedWidgetOfExactType
on 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! 🐈