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.
How helpful was this content?
Please sign in to rate this article.