WidgetsBindingObserver: Handling App Lifecycle in Flutter
Flutter apps live inside an OS lifecycle they do not control. WidgetsBindingObserver is how your widgets learn about resume, pause, memory pressure, and metric changes — and how to use it correctly.
Published on • October 13, 2026
AI Assistant

Your Flutter app does not run in a vacuum. The user swipes it to the background, receives a phone call, rotates the screen, plugs in a low-memory situation, or flips the system to dark mode. Each of those events changes what your code should be doing — and if you never hear about them, you end up with timers burning battery in the background, animations running while paused, or cameras that never release.
WidgetsBindingObserver is the interface that delivers those events to your widgets.
What the observer actually observes
WidgetsBindingObserver is a mixin interface with a set of didChange… callbacks. The most important is app lifecycle state:
mixin class MyScreenState extends State<MyPage> 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) {
switch (state) {
case AppLifecycleState.resumed:
startTimers();
case AppLifecycleState.paused:
case AppLifecycleState.hidden:
case AppLifecycleState.detached:
stopTimers();
case AppLifecycleState.inactive:
break; // transient, e.g. incoming call UI or app switcher
}
}
}
Two rules are easy to get wrong and worth engraving:
- You must register —
addObserver(this)ininitState, andremoveObserver(this)indispose. Forgetting the removal leaks your State and keeps dead callbacks firing. - Registration is on the binding, not the widget tree — the observer lives as long as you keep it registered, which is exactly as long as your State object.
The lifecycle states, honestly
Modern Flutter defines five states, and the semantics have subtle edges:
resumed— visible and rendering normally.inactive— visible but not receiving input; a transient state during transitions (incoming call overlay, app switcher, dismiss gestures).hidden— fully occluded but still alive. Added in Flutter 3.13 alongsideAppLifecycleListener; it is the modern stand-in for “not visible anymore.”paused— not visible; the framework stops rendering. This is where you stop expensive work. On some platforms the OS may suspend you shortly after.detached— the engine is running with no views; typically during teardown.
The subtle edge: inactive is not “background.” For years, code written against older docs treated inactive as the moment to pause everything, which caused visible flicker during transient transitions. hidden/paused is the correct pair for pause logic; treat inactive as “maybe something is happening, hold steady.”
The other observers you are ignoring
App lifecycle is the headline, but the interface delivers more:
@override
void didChangeMetrics() {
// window/view metrics changed: rotation, fold, split-screen resize
}
@override
void didChangeTextScaleFactor() {
// user changed system font size — recheck your layouts
}
@override
void didChangePlatformBrightness() {
// system dark mode flipped — update your ThemeMode
}
@override
void didChangeAccessibilityFeatures() { ... }
@override
void didHaveMemoryPressure() {
// OS says memory is tight — drop caches, shed non-essential state
}
didChangeMetrics matters for anything with custom canvases, maps, or embedded video: on foldables and split-screen, your logical size can change without an orientation change.
The modern alternative: AppLifecycleListener
Since Flutter 3.13, there is an object-oriented complement. Instead of mixing in an interface with a switch, you subscribe to specific states:
late final AppLifecycleListener _listener;
AppLifecycleState? _lastState;
@override
void initState() {
super.initState();
_listener = AppLifecycleListener(
onShow: () => _log('show'),
onResume: () => startTimers(),
onHide: () => _log('hide'),
onPause: () => stopTimers(),
onDetach: () => _log('detach'),
onExitRequested: () async => AppExitResponse.exit, // desktop/Windows
);
}
@override
void dispose() {
_listener.dispose();
super.dispose();
}
WidgetsBindingObserver is not deprecated — but for lifecycle specifically, AppLifecycleListener gives you named callbacks and avoids the switch entirely. You will still want the observer for the non-lifecycle events (didChangeMetrics, memory pressure), so many apps keep both.
Practical rules for Flutter teams
- Pause work, not just UI. Timers, geolocation streams, camera sessions, and websocket reconnect loops all need explicit lifecycle handling. The framework stops rendering when paused; it does not stop your code.
- Never block
didChangeAppLifecycleState. It runs on the main isolate. Delegate heavy work. - Test it.
WidgetsBinding.handleAppLifecycleStateChanged(...)lets you drive states in widget tests without a real device. - Do not assume one notification. Transitions can deliver rapid sequences (
resumed → inactive → hidden → paused); make handlers idempotent.
Further reading
- WidgetsBindingObserver API reference — the full callback list and semantics
- AppLifecycleState enum — the five states and their transitions
- AppLifecycleListener — the modern per-state callback API
- App lifecycle sample — official guidance on reacting to lifecycle
Ignoring lifecycle is the Flutter equivalent of leaving the lights on in an empty office: nobody notices for an hour, then the battery report arrives. A registered observer is a five-line habit that keeps your app honest about where it actually is.