Flutter Lesson 9 of 83 3 min read
The StatefulWidget Lifecycle in Flutter
Learn the lifecycle of a StatefulWidget: initState, didChangeDependencies, build, didUpdateWidget and dispose, with what belongs in each.
On this page
A State object goes through a fixed sequence of steps from creation to removal. Each step has a method you can override.
The methods, in order #
| Method | Called | Use it to |
|---|---|---|
initState() | Once, when the state is created | Set initial values, create controllers, start loading data |
didChangeDependencies() | After initState, and whenever an inherited widget it depends on changes | Read values that need context, such as the theme |
build() | Whenever the widget needs drawing | Describe the UI. Nothing else |
didUpdateWidget(old) | When the parent rebuilds with new parameters | React to a changed parameter |
dispose() | Once, when the state is removed for good | Clean up: controllers, timers, subscriptions |
A complete example #
import 'dart:async';
import 'package:flutter/material.dart';
class Clock extends StatefulWidget {
const Clock({super.key});
@override
State<Clock> createState() => _ClockState();
}
class _ClockState extends State<Clock> {
late Timer _timer;
DateTime _now = DateTime.now();
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
setState(() => _now = DateTime.now());
});
}
@override
void dispose() {
_timer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
String two(int n) => n.toString().padLeft(2, '0');
return Text(
'${two(_now.hour)}:${two(_now.minute)}:${two(_now.second)}',
style: Theme.of(context).textTheme.displaySmall,
);
}
}
void main() {
runApp(const MaterialApp(home: Scaffold(body: Center(child: Clock()))));
}
Two rules shown here:
- Call
super.initState()first ininitState. - Call
super.dispose()last indispose.
Whatever you create, dispose #
These all hold resources and must be cleaned up in dispose:
TextEditingController,ScrollController,FocusNodeAnimationControllerTimerStreamSubscription
Forgetting causes memory leaks and “setState called after dispose” errors.
initState cannot use context for everything #
In initState the widget is not fully in the tree yet, so calls like Theme.of(context) and MediaQuery.of(context) are not allowed. Put them in didChangeDependencies or in build.
initState also cannot be async. To load data, call an async method from it.
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
final user = await fetchUser();
if (!mounted) return;
setState(() => _user = user);
}
Reacting to changed parameters #
@override
void didUpdateWidget(covariant UserCard oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
_load(); // the parent now wants a different user
}
}
The app’s own lifecycle #
To know when the whole app goes to the background or returns, use AppLifecycleListener.
late final AppLifecycleListener _listener;
@override
void initState() {
super.initState();
_listener = AppLifecycleListener(
onResume: () => debugPrint('App is visible again'),
onPause: () => debugPrint('App went to the background'),
);
}
@override
void dispose() {
_listener.dispose();
super.dispose();
}
Typical uses: pause a video, save a draft, refresh data on return.
Try it yourself #
Write a Stopwatch widget with Start, Stop and Reset buttons that shows elapsed seconds. Create the timer when Start is pressed and make sure it is cancelled in dispose.