Flutter Life Cycle - StatefulWidget and AppLifecycleState Management
Flutter Life Cycle
The first time I needed to make an API call, I wondered "Where should I write this?" In the build() method? No, because it would be called every time the screen refreshes. This is where the life cycle comes into play.
In Flutter, there are two types of widgets: StatelessWidget and StatefulWidget. StatelessWidget doesn't have a special life cycle (because it doesn't change), but StatefulWidget has a very rich life cycle.
Live Demo: Life Cycle Methods
Try StatefulWidget life cycle methods interactively:
StatefulWidget Life Cycle
A StatefulWidget's life progresses like this:
createState() → initState() → didChangeDependencies() → build()
↓
setState() ←→ build() ←→ didUpdateWidget()
↓
dispose()1. createState()
Called when the widget is first created. Creates the State object.
class MyWidget extends StatefulWidget {
@override
_MyWidgetState createState() {
print('createState executed');
return _MyWidgetState();
}
}When called: When the widget is created, only once.
What it's for: Returning the State class. Usually doesn't need customization.
2. initState()
Called immediately after the State object is created. Initial setup is done here.
class _MyWidgetState extends State<MyWidget> {
int counter = 0;
late Timer timer;
@override
void initState() {
super.initState(); // Must call this!
print('initState executed');
// Initial setup
counter = 0;
_loadData();
_startTimer();
}
void _loadData() async {
// API call
final data = await fetchDataFromAPI();
setState(() {
// Save data to state
});
}
void _startTimer() {
timer = Timer.periodic(Duration(seconds: 1), (timer) {
setState(() {
counter++;
});
});
}
@override
Widget build(BuildContext context) {
return Text('Counter: $counter');
}
}When called: When the widget is first created, only once.
What it's for:
- Assigning initial values to variables
- Making API calls
- Creating streams/controllers
- Starting timers
- Adding event listeners
Important: Don't forget to call super.initState()!
3. didChangeDependencies()
Called when the widget's dependencies change.
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies executed');
// Did theme change?
final theme = Theme.of(context);
// Did MediaQuery change? (screen size, orientation)
final screenSize = MediaQuery.of(context).size;
}When called:
- Right after
initState() - When InheritedWidget changes (Theme, MediaQuery, etc.)
What it's for:
- Operations dependent on context
- Catching Theme, Locale changes
Note: Can't use context in initState(), but can use it here.
4. build()
Creates the widget's view.
@override
Widget build(BuildContext context) {
print('build executed');
return Scaffold(
appBar: AppBar(title: Text('Page')),
body: Center(
child: Text('Counter: $counter'),
),
);
}When called:
- After
didChangeDependencies() - Every time
setState()is called - After
didUpdateWidget() - When parent widget rebuilds
What it's for: Creating UI.
Important:
- Don't make API calls here!
- Don't start timers here!
- Can be called frequently, watch performance.
5. didUpdateWidget()
Called when parent widget changes and sends a new configuration.
class ParentWidget extends StatefulWidget {
final String title;
ParentWidget({required this.title});
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
@override
void didUpdateWidget(ParentWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget executed');
// Compare old and new widget
if (oldWidget.title != widget.title) {
print('Title changed: ${oldWidget.title} → ${widget.title}');
// Make necessary updates
}
}
@override
Widget build(BuildContext context) {
return Text(widget.title);
}
}When called: When parent widget changes and rebuilds.
What it's for:
- Comparing old and new widgets
- Updating based on changed parameters
6. setState()
Used to update state and rebuild the widget.
class _CounterState extends State<Counter> {
int count = 0;
void _increment() {
setState(() {
count++; // State change
});
// At this point, build() is called again
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Text('Count: $count'),
ElevatedButton(
child: Text('Increment'),
onPressed: _increment,
),
],
);
}
}When called: You call it manually.
What it's for: Notifying Flutter of state changes.
Important:
- Be careful with async operations
- Avoid unnecessary setState calls
- Don't call after widget is disposed
7. dispose()
Called before the widget is removed. Cleanup is done here.
class _MyWidgetState extends State<MyWidget> {
late Timer timer;
late StreamSubscription subscription;
TextEditingController controller = TextEditingController();
@override
void initState() {
super.initState();
timer = Timer.periodic(Duration(seconds: 1), (timer) {
// ...
});
subscription = someStream.listen((data) {
// ...
});
}
@override
void dispose() {
print('dispose executed - cleaning up');
// Cancel timer
timer.cancel();
// Close stream
subscription.cancel();
// Dispose controller
controller.dispose();
super.dispose(); // Must call this last!
}
@override
Widget build(BuildContext context) {
return Container();
}
}When called: Before the widget is removed from the widget tree.
What it's for:
- Stopping timers
- Closing streams
- Disposing controllers
- Removing event listeners
- Preventing memory leaks
Important: super.dispose() must be called last!
Practical Example: API Call with Life Cycle
class UserProfilePage extends StatefulWidget {
final int userId;
UserProfilePage({required this.userId});
@override
_UserProfilePageState createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
bool isLoading = true;
String? userName;
String? errorMessage;
@override
void initState() {
super.initState();
print('1. initState - Making API call');
_loadUserData();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print('2. didChangeDependencies - Context ready');
}
@override
void didUpdateWidget(UserProfilePage oldWidget) {
super.didUpdateWidget(oldWidget);
print('3. didUpdateWidget - Did userId change?');
// Reload if userId changed
if (oldWidget.userId != widget.userId) {
setState(() {
isLoading = true;
errorMessage = null;
});
_loadUserData();
}
}
Future<void> _loadUserData() async {
try {
print('API call started: userId=${widget.userId}');
// Simulated API call
await Future.delayed(Duration(seconds: 2));
// Check if widget is still mounted
if (!mounted) return;
setState(() {
userName = 'User ${widget.userId}';
isLoading = false;
});
print('API call completed');
} catch (e) {
if (!mounted) return;
setState(() {
errorMessage = 'Error: $e';
isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
print('4. build - Creating UI');
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Center(
child: isLoading
? CircularProgressIndicator()
: errorMessage != null
? Text(errorMessage!)
: Text('Hello, $userName!'),
),
);
}
@override
void dispose() {
print('5. dispose - Cleaning up widget');
super.dispose();
}
}Application Life Cycle (App Life Cycle)
Different from widget life cycle, the application itself also has a life cycle.
AppLifecycleState
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
super.didChangeAppLifecycleState(state);
switch (state) {
case AppLifecycleState.resumed:
print('App active - User in app');
// Update data, start animations
break;
case AppLifecycleState.inactive:
print('App inactive - Temporary state');
// Phone call, notification pull down
break;
case AppLifecycleState.paused:
print('App in background');
// Save data, stop operations
break;
case AppLifecycleState.detached:
print('App terminating');
// Final cleanup
break;
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(home: HomePage());
}
}State Descriptions
- resumed: App is visible and user is interacting
- inactive: App is visible but no interaction (phone call)
- paused: App is in background, not visible
- detached: App is terminating
Practical Use: Video Player
class VideoPlayerPage extends StatefulWidget {
@override
_VideoPlayerPageState createState() => _VideoPlayerPageState();
}
class _VideoPlayerPageState extends State<VideoPlayerPage>
with WidgetsBindingObserver {
late VideoPlayerController videoController;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
videoController = VideoPlayerController.network('video_url');
videoController.initialize();
videoController.play();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// App moved to background - pause video
videoController.pause();
} else if (state == AppLifecycleState.resumed) {
// App active - resume video
videoController.play();
}
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
videoController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AspectRatio(
aspectRatio: videoController.value.aspectRatio,
child: VideoPlayer(videoController),
),
),
);
}
}deactivate() Method
Rarely used, called when widget is temporarily removed from the tree.
@override
void deactivate() {
super.deactivate();
print('Widget deactivated');
// Temporary cleanup operations
}When called:
- During page changes with Navigator
- When widget is repositioned
Common Mistakes
1. Using Context in initState
// ❌ Wrong
@override
void initState() {
super.initState();
final theme = Theme.of(context); // ERROR!
}
// ✅ Correct
@override
void didChangeDependencies() {
super.didChangeDependencies();
final theme = Theme.of(context); // Correct
}2. setState After dispose
// ❌ Wrong
Future<void> loadData() async {
final data = await api.fetchData();
setState(() {
this.data = data; // Widget might be disposed!
});
}
// ✅ Correct
Future<void> loadData() async {
final data = await api.fetchData();
if (mounted) { // mounted check
setState(() {
this.data = data;
});
}
}3. Side Effects in build()
// ❌ Wrong
@override
Widget build(BuildContext context) {
fetchData(); // Called on every build!
return Text('...');
}
// ✅ Correct
@override
void initState() {
super.initState();
fetchData(); // Called once
}4. Forgetting super Call
// ❌ Wrong
@override
void initState() {
// No super.initState()!
loadData();
}
// ✅ Correct
@override
void initState() {
super.initState(); // Must call this!
loadData();
}Best Practices
- Always check mounted (for async operations)
- Clean up in dispose (timers, streams, controllers)
- Don't forget to call super methods
- Keep initState minimal (use Future.delayed for heavy operations)
- Keep build() pure (no side effects)
Summary
StatefulWidget Life Cycle:
createState()- Create StateinitState()- Initial setupdidChangeDependencies()- Dependency changebuild()- Create UIdidUpdateWidget()- Parent changesetState()- Update statedispose()- Cleanup
App Life Cycle:
resumed- Activeinactive- Temporarily inactivepaused- Backgrounddetached- Terminating
Understanding life cycle methods is fundamental to writing performant and bug-free Flutter applications!
Confused about life cycle?
See you in the next article! 🚀