The Flutter Lifecycle
Flutter’s State<T> class exposes a set of lifecycle methods that fire in a well-defined order. If you have used React hooks or class components, every single one of these has a direct equivalent. Once you internalize the mapping you will know exactly when to put each piece of logic.
The complete mapping table
Section titled “The complete mapping table”| Flutter method | When it fires | React equivalent |
|---|---|---|
createState() | Once — widget first inserted into tree | Component function first call / constructor() |
initState() | Once — State object created, before first build() | useEffect(() => { /* setup */ }, []) — mount phase |
didChangeDependencies() | After initState(), and when an InheritedWidget the State depends on changes | useEffect(() => { /* context */ }, [contextValue]) |
build() | Every time the widget needs to redraw | Component function body / render() |
didUpdateWidget(oldWidget) | Parent rebuilds and passes a new widget configuration | useEffect(() => { /* sync */ }, [prop]) |
setState(() { ... }) | Called by your code to mutate state and trigger a rebuild | const [x, setX] = useState(...) setter call |
deactivate() | Widget removed from tree temporarily (uncommon) | (no direct equivalent — rarely needed) |
dispose() | State permanently destroyed | useEffect(() => { return () => cleanup(); }, []) |
Step-by-step lifecycle flow
Section titled “Step-by-step lifecycle flow”createState() ↓initState() ← mount: start controllers, subscriptions, fetch initial data ↓didChangeDependencies() ← first call, then fires again if InheritedWidget changes ↓build() ← pure UI description; runs every rebuild ↑ ↓setState() ← loops back to build() until widget is removed ↓didUpdateWidget() ← fires when parent passes new widget config (new props) ↓deactivate() ← tree removal (usually temporary) ↓dispose() ← permanent teardown: cancel timers, close streams, dispose controllersSide-by-side comparison
Section titled “Side-by-side comparison”import { useState, useEffect, useRef } from 'react';
function TimerWidget({ label }) { const [seconds, setSeconds] = useState(0); const intervalRef = useRef(null);
// Mount — equivalent to initState useEffect(() => { intervalRef.current = setInterval(() => { setSeconds(s => s + 1); }, 1000);
// Unmount cleanup — equivalent to dispose return () => clearInterval(intervalRef.current); }, []);
// Prop change — equivalent to didUpdateWidget useEffect(() => { console.log('label changed to', label); // re-sync any logic that depends on label }, [label]);
return ( <div> <p>{label}: {seconds}s</p> </div> );}class TimerWidget extends StatefulWidget { final String label; const TimerWidget({super.key, required this.label});
@override State<TimerWidget> createState() => _TimerWidgetState();}
class _TimerWidgetState extends State<TimerWidget> { int _seconds = 0; Timer? _timer;
// Mount — runs once before first build() @override void initState() { super.initState(); _timer = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted) setState(() => _seconds++); }); }
// Prop change — old config available via oldWidget @override void didUpdateWidget(TimerWidget oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.label != widget.label) { debugPrint('label changed to ${widget.label}'); } }
// Unmount — permanent teardown @override void dispose() { _timer?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) { return Text('${widget.label}: $_seconds s', style: const TextStyle(fontSize: 24)); }}Runnable example — Timer with full lifecycle
Section titled “Runnable example — Timer with full lifecycle”import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple)),
home: const LifecycleDemoScreen(),
);
}
}
class LifecycleDemoScreen extends StatefulWidget {
const LifecycleDemoScreen({super.key});
@override
State<LifecycleDemoScreen> createState() => _LifecycleDemoScreenState();
}
class _LifecycleDemoScreenState extends State<LifecycleDemoScreen> {
bool _showTimer = true;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Lifecycle Demo')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_showTimer) const TimerWidget(label: 'Elapsed'),
const SizedBox(height: 32),
FilledButton(
onPressed: () => setState(() => _showTimer = !_showTimer),
child: Text(_showTimer ? 'Unmount timer (dispose)' : 'Mount timer (initState)'),
),
],
),
),
);
}
}
// The widget that demonstrates the full lifecycle
class TimerWidget extends StatefulWidget {
final String label;
const TimerWidget({super.key, required this.label});
@override
State<TimerWidget> createState() => _TimerWidgetState();
}
class _TimerWidgetState extends State<TimerWidget> {
int _seconds = 0;
Timer? _timer;
final List<String> _log = [];
// ═══════════════ initState ═══════════════
// Equivalent to: useEffect(() => { start timer }, [])
@override
void initState() {
super.initState();
_log.add('initState — timer started');
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (mounted) {
setState(() {
_seconds++;
_log.add('setState — rebuild #$_seconds');
});
}
});
}
// ═══════════════ didChangeDependencies ═══════════════
// Fires after initState and on InheritedWidget changes
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Safe to call Theme.of(context) here
_log.add('didChangeDependencies');
}
// ═══════════════ didUpdateWidget ═══════════════
// Equivalent to: useEffect(() => { sync }, [label])
@override
void didUpdateWidget(TimerWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.label != widget.label) {
_log.add('didUpdateWidget — label changed');
}
}
// ═══════════════ dispose ═══════════════
// Equivalent to: useEffect(() => { return () => cleanup() }, [])
@override
void dispose() {
_timer?.cancel();
// Note: setState cannot be called here — widget is being torn down
super.dispose();
}
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${widget.label}: $_seconds s',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: scheme.primary,
),
),
const SizedBox(height: 16),
Container(
width: 320,
height: 160,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: scheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
reverse: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _log
.map((entry) => Text(entry,
style: TextStyle(fontSize: 12, color: scheme.onSurface)))
.toList(),
),
),
),
],
);
}
}