WidgetsBindingObserver
3 min read
Lifecycle & Platform
| Callback | Fires when |
|---|---|
didChangeAppLifecycleState | App moves between resumed / inactive / paused / hidden / detached |
didChangeMetrics | Window size or screen insets change (rotation, keyboard, foldable hinge) |
didChangePlatformBrightness | System dark/light mode toggles |
didChangeLocales | User changes system language |
didHaveMemoryPressure | OS signals low memory |
didChangeAccessibilityFeatures | Accessibility settings change |
didRequestAppExit | OS asks to confirm exit (desktop) |
Register once at app root (or a high-up widget). Don't sprinkle observers across the tree — they all fire on every event.
Code in action
class AppLifecycle extends StatefulWidget {
const AppLifecycle({super.key, required this.child});
final Widget child;
@override
State<AppLifecycle> createState() => _AppLifecycleState();
}
class _AppLifecycleState extends State<AppLifecycle>
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: _refresh(); _connect(); // foreground
case AppLifecycleState.inactive: /* call, control center */
case AppLifecycleState.paused: _saveState(); _disconnect(); // backgrounded
case AppLifecycleState.hidden: /* desktop only */
case AppLifecycleState.detached: _cleanup(); // terminating
}
}
@override
void didChangePlatformBrightness() {
// react to dark/light flip
}
@override
Widget build(BuildContext context) => widget.child;
}
When to use it
| Need | Use |
|---|---|
| Pause polling / sockets when app backgrounded | didChangeAppLifecycleState |
| Refresh data when app returns | didChangeAppLifecycleState(.resumed) |
| Save state before terminate | didChangeAppLifecycleState(.detached) (limited time on mobile) |
| React to keyboard / rotation | didChangeMetrics (or MediaQuery for build-time reactivity) |
| React to system dark mode | didChangePlatformBrightness |
| Just need current lifecycle in build | AppLifecycleListener (Flutter 3.13+) — newer, more granular API |
Common mistakes to avoid
// ❌ Adding observer without removing it → leak across hot reloads
// ✅ Always pair addObserver / removeObserver in initState / dispose
// ❌ Registering an observer in every screen
// → N observers all reacting to every lifecycle event
// ✅ One observer near the app root; broadcast as needed via Provider/Riverpod/EventBus
// ❌ Doing heavy async work in didChangeAppLifecycleState(.paused) / .detached
// On mobile, you get a few seconds at best — long writes may not finish
// ✅ Persist incrementally throughout the session; the lifecycle hook is a final flush, not the only save
// ❌ Calling setState from didChangeMetrics on every keyboard toggle
// → unnecessary rebuilds
// ✅ Use MediaQuery (which already rebuilds the right subtree) unless you specifically need the event
// ❌ Assuming AppLifecycleState.detached always fires
// It's "best effort." Don't rely on it for critical persistence.
Interview follow-ups
-
What's the difference between
didChangeAppLifecycleState(.paused)anddispose()?pausedfires when the app is backgrounded (still alive, can return).disposefires when the State itself is removed from the tree. Different scopes: lifecycle is process-wide, dispose is widget-local. -
When would you use
AppLifecycleListenerinstead ofWidgetsBindingObserver?AppLifecycleListener(Flutter 3.13+) is the modern, more granular API — you register callbacks individually (onResume,onPause,onDetach) and get exit-cancellation support (onExitRequested). For new code, prefer it; for older codebases or when you also need metrics/locale callbacks,WidgetsBindingObserveris still fine. -
Why pair
addObserver/removeObserverwith mounted widget lifecycle? Because the observer holds a reference to your State. ForgetremoveObserverand the State (and everything it captures) leaks. Always register ininitState, unregister indispose. -
What's the cost of
didChangeMetricsfiring? The callback itself is cheap, but if you callsetStatefrom it, the consequence is a rebuild. Keyboard show/hide triggersdidChangeMetrics, so any observer that rebuilds the app on every metric change will jank the keyboard animation. PreferMediaQuery— it rebuilds only the parts that depend on the changed value.
How helpful was this content?
Please sign in to rate this article.