WidgetsBindingObserver

Medium PriorityAsked in ~50% of mid-level interviews

3 min read

Lifecycle & Platform

CallbackFires when
didChangeAppLifecycleStateApp moves between resumed / inactive / paused / hidden / detached
didChangeMetricsWindow size or screen insets change (rotation, keyboard, foldable hinge)
didChangePlatformBrightnessSystem dark/light mode toggles
didChangeLocalesUser changes system language
didHaveMemoryPressureOS signals low memory
didChangeAccessibilityFeaturesAccessibility settings change
didRequestAppExitOS 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

NeedUse
Pause polling / sockets when app backgroundeddidChangeAppLifecycleState
Refresh data when app returnsdidChangeAppLifecycleState(.resumed)
Save state before terminatedidChangeAppLifecycleState(.detached) (limited time on mobile)
React to keyboard / rotationdidChangeMetrics (or MediaQuery for build-time reactivity)
React to system dark modedidChangePlatformBrightness
Just need current lifecycle in buildAppLifecycleListener (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

  1. What's the difference between didChangeAppLifecycleState(.paused) and dispose()? paused fires when the app is backgrounded (still alive, can return). dispose fires when the State itself is removed from the tree. Different scopes: lifecycle is process-wide, dispose is widget-local.

  2. When would you use AppLifecycleListener instead of WidgetsBindingObserver? 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, WidgetsBindingObserver is still fine.

  3. Why pair addObserver / removeObserver with mounted widget lifecycle? Because the observer holds a reference to your State. Forget removeObserver and the State (and everything it captures) leaks. Always register in initState, unregister in dispose.

  4. What's the cost of didChangeMetrics firing? The callback itself is cheap, but if you call setState from it, the consequence is a rebuild. Keyboard show/hide triggers didChangeMetrics, so any observer that rebuilds the app on every metric change will jank the keyboard animation. Prefer MediaQuery — it rebuilds only the parts that depend on the changed value.


How helpful was this content?

Please sign in to rate this article.