The lifecycle is the sequence of states through which a widget passes.
In the previous article we saw that a StatefulWidget separates its immutable configuration from the State object, which is created, updated, and destroyed along with a position in the tree.
We call this sequence of events the Lifecycle.
Understanding when each phase occurs is important. If you access a tree dependency too early, you’ll get an error. If you don’t release resources when the State disappears, you can cause errors or memory leaks.
Let’s dissect these stages.
Initialization with initState()
When Flutter inserts a StatefulWidget into the widget tree, the first thing it does (after the constructor) is execute the initState() method.
This method runs only once. It’s the perfect moment for initial setup.
What do we use it for?
- Initialize variables that depend on the widget’s configuration.
- Subscribe to Streams.
- Initialize Controllers (like
TextEditingControllerfor forms orAnimationControllerfor animations). - Load initial data (though be careful with making HTTP requests here without properly managing asynchrony).
@override
void initState() {
super.initState(); // ⚠️ ALWAYS call super at the beginning
// Your initialization code here
print("The Widget has just been born");
_startTimer();
}The build() method hasn’t been called yet. Do not use dependOnInheritedWidgetOfExactType or APIs that depend on InheritedWidget, like Theme.of(context), here; that’s what didChangeDependencies() is for, which is called right after.
Rebuilding with build() and setState()
Once initialized, the widget enters its active phase. Flutter calls the build() method to render the interface.
As we saw in the previous article, each time we call setState(), the cycle repeats at this point: Flutter re-executes build() to reflect the changes.
Do not put heavy logic or initiate API calls inside build(). Flutter can invoke it many times, so this method should be fast and have no side effects.
Cleanup with dispose()
This is where many developers fail. When a widget is no longer needed (for example, because the user navigated to another screen or deleted an item from a list), Flutter destroys it.
Just before destroying it, it calls the dispose() method.
This is the moment to clean up. If you opened a tap in initState, you must close it in dispose.
What needs to be cleaned up?
- Timers: If you don’t stop them, they will keep counting in the background even if the screen no longer exists, throwing errors.
- Controllers: Text and animation controllers occupy native memory. They must be closed.
- Streams: You must cancel subscriptions to stop listening to data.
@override
void dispose() {
// Cleanup before destruction
_timer.cancel();
_controller.dispose();
super.dispose(); // ⚠️ ALWAYS call super at the end
print("The Widget has died and we clean up the memory");
}Practical example: a stopwatch
Let’s see an example where the lifecycle really matters. We’ll make a text that counts seconds.
If we don’t use dispose, and the user leaves the screen, the stopwatch would keep counting in the phone’s memory, wasting resources unnecessarily (Memory Leak).
import 'dart:async'; // Needed for Timer
import 'package:flutter/material.dart';
class CronometroWidget extends StatefulWidget {
@override
_CronometroWidgetState createState() => _CronometroWidgetState();
}
class _CronometroWidgetState extends State<CronometroWidget> {
int _seconds = 0;
Timer? _timer; // Variable to control the time
@override
void initState() {
super.initState();
// BIRTH: Start the timer
_startCounting();
}
void _startCounting() {
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
// LIFE: Update the state every second
setState(() {
_seconds++;
});
});
}
@override
void dispose() {
// DEATH: Very important! Cancel the timer
// If we don't do this, the timer will try to call setState()
// on a widget that no longer exists, and the app will crash.
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Lifecycle")),
body: Center(
child: Text(
'$_seconds',
style: TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
),
),
);
}
}