Skip to content

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.

Flutter methodWhen it firesReact equivalent
createState()Once — widget first inserted into treeComponent 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 changesuseEffect(() => { /* context */ }, [contextValue])
build()Every time the widget needs to redrawComponent function body / render()
didUpdateWidget(oldWidget)Parent rebuilds and passes a new widget configurationuseEffect(() => { /* sync */ }, [prop])
setState(() { ... })Called by your code to mutate state and trigger a rebuildconst [x, setX] = useState(...) setter call
deactivate()Widget removed from tree temporarily (uncommon)(no direct equivalent — rarely needed)
dispose()State permanently destroyeduseEffect(() => { return () => cleanup(); }, [])
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 controllers
React
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>
);
}
Flutter
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(),
            ),
          ),
        ),
      ],
    );
  }
}
Which lifecycle method is equivalent to useEffect(() => { ... }, []) — the mount-only effect?
What is the correct place to cancel a Timer or close a StreamSubscription?
didUpdateWidget(oldWidget) fires when:
Why must you call super.dispose() at the END (not the start) of your dispose() override?