skip to content

In Flutter, how does AppLifecycleListener differ from mixing WidgetsBindingObserver into a State to react to app lifecycle changes?

level: middleimportance: should knowfreq 38%

answer

  1. an object, not a mixin
  2. remembers the previous state
  3. onShow versus onInactive
  4. onExitRequested returns AppExitResponse
  5. dispose() removes the observer

basics

~10 s

AppLifecycleListener, added in Flutter 3.13, is a disposable object that registers itself on WidgetsBinding and turns state changes into transition callbacks such as onHide, onShow and onExitRequested; WidgetsBindingObserver only hands you the new state.

solid answer

~30 s

With the mixin you call `WidgetsBinding.instance.addObserver(this)`, override `didChangeAppLifecycleState`, and get only the new state, so you track direction yourself. `AppLifecycleListener` is an object that adds itself to the binding in its constructor and removes itself in `dispose()`. It remembers the previous state and calls `onResume`, `onInactive`, `onHide`, `onShow`, `onPause`, `onRestart` and `onDetach` for specific transitions, plus `onStateChange` for every change and `onExitRequested` to cancel a cancelable desktop exit. It needs no `State`, so a controller can own it. Keep the mixin when you also need `didChangeMetrics`, `didHaveMemoryPressure` or other binding callbacks.

code

dart · 34 lines
dart
import 'package:flutter/widgets.dart';

class VideoCallScreen extends StatefulWidget {
  const VideoCallScreen({super.key});

  @override
  State<VideoCallScreen> createState() => _VideoCallScreenState();
}

class _VideoCallScreenState extends State<VideoCallScreen> {
  late final AppLifecycleListener _lifecycle;
  bool _sendingVideo = true;

  @override
  void initState() {
    super.initState();
    _lifecycle = AppLifecycleListener(
      onHide: () => setState(() => _sendingVideo = false),
      onShow: () => setState(() => _sendingVideo = true),
      onStateChange: (AppLifecycleState state) => debugPrint('call screen: $state'),
    );
  }

  @override
  void dispose() {
    _lifecycle.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(child: Text(_sendingVideo ? 'Camera on' : 'Camera paused'));
  }
}

go deeper

for a junior

Remember that both approaches need cleanup in dispose, and that the listener is the newer, callback-based API.

for a middle

Explain how the listener derives transitions from the previous state and why onShow and onInactive both land on inactive.

for a senior

Show when you keep the mixin for metrics or memory pressure, and how you move lifecycle handling out of widgets into a disposable service.

for a principal

Discuss where lifecycle reactions should live in a layered app so features do not each register observers, and how exit confirmation differs on desktop.

## Two ways to hear about the lifecycle Flutter delivers lifecycle changes through **`WidgetsBinding`**, the singleton that connects the widget layer to the engine. Anything that wants to hear about them registers as a **`WidgetsBindingObserver`**. There are two ways to do that. **The classic way** is to mix `WidgetsBindingObserver` (an `abstract mixin class` in the widgets library) into a `State`, register it with `WidgetsBinding.instance.addObserver(this)` in `initState`, unregister with `WidgetsBinding.instance.removeObserver(this)` in `dispose`, and override `didChangeAppLifecycleState(AppLifecycleState state)`. The method receives only the **new** state. **The newer way**, added in Flutter 3.13, is **`AppLifecycleListener`**: an ordinary object that is itself a `WidgetsBindingObserver`. Its constructor adds it to the binding; its `dispose()` removes it. You pass callbacks instead of overriding methods. ## What the listener adds `AppLifecycleListener` remembers the previous state and turns each change into a named **transition**, so you do not have to track "where did I come from?" yourself: | Callback | Fires when the state changes | Platforms | |---|---|---| | `onResume` | to `resumed` | all | | `onInactive` | `resumed` to `inactive` | all | | `onHide` | `inactive` to `hidden` | all | | `onShow` | `hidden` to `inactive` | all | | `onPause` | `hidden` to `paused` | iOS, Android | | `onRestart` | `paused` to `hidden` | iOS, Android | | `onDetach` | to `detached` | iOS, Android | | `onStateChange` | any change, with the new value | all | | `onExitRequested` | a cancelable exit request | desktop | Note the pairs: arriving at `inactive` calls `onShow` when coming up from `hidden` but `onInactive` when coming down from `resumed`. With a raw observer both cases look identical, because `didChangeAppLifecycleState(AppLifecycleState.inactive)` carries no direction. ## Exit requests `onExitRequested` returns a `Future<AppExitResponse>`. Returning `AppExitResponse.cancel` cancels a **cancelable** exit, for example to ask "discard unsaved changes?"; `AppExitResponse.exit` lets it proceed. Without the callback the listener answers `exit`. The framework docs note that cancelable exit requests are currently supported only on macOS and Linux, and that many exits (a kill, a power loss) are never cancelable, so this is not a place to save critical data. The raw observer equivalent is overriding `didRequestAppExit`; if any observer answers `cancel`, the exit is cancelled. ## When to still use the mixin `WidgetsBindingObserver` remains the right tool when you need its **other** callbacks, which the listener does not expose: - `didChangeMetrics` for window size and inset changes; - `didChangePlatformBrightness` and `didChangeLocales`; - `didChangeAccessibilityFeatures`; - `didHaveMemoryPressure` to drop caches. For lifecycle alone, the listener is the more readable choice and does not require a `State`: a controller, a repository or a Riverpod-managed service can own one. ## Rules that apply to both 1. **Register once, unregister once.** A listener created in `initState` must be disposed in `dispose`; an observer added in `initState` must be removed in `dispose`. Forgetting leaks the `State` through the binding, and callbacks keep firing after the widget is gone. 2. **Do not create a listener in `build`.** Every rebuild would register another observer. 3. **Expect synthesized steps.** Both receive every intermediate state, in order, because the binding expands jumps into chains. 4. **Keep handlers short.** They run on the UI isolate; long synchronous work there delays the next frame. ## Choosing in practice - **New lifecycle-only code:** use `AppLifecycleListener`. The callbacks name the transition, so the handler reads like the requirement ("stop sending video when hidden"). - **Existing screens built on the mixin:** there is no need to migrate for its own sake; both receive the same states in the same order. - **Code that also needs metrics, brightness, locales or memory pressure:** keep `WidgetsBindingObserver`, or combine an observer for those with a listener for lifecycle. - **Tests:** the listener takes an optional `binding` argument that defaults to `WidgetsBinding.instance`, which its documentation says may be substituted for testing or specialized bindings. - **Many features reacting to the same change:** several listeners are allowed, since each is simply another observer, but one app-level service that fans out events keeps the reactions in one place and easier to reason about.

  • Why does AppLifecycleListener call onShow rather than onInactive when a phone app comes back from the background?
    Coming back runs `paused`, `hidden`, `inactive`, `resumed`. Arriving at `inactive` from `hidden` is a show transition, so `onShow` fires; `onInactive` is reserved for leaving `resumed`. The listener can tell them apart because it stores the previous state.
  • Can onExitRequested stop a user from swiping a Flutter app away on Android?
    No. It answers only cancelable exit requests, which the framework docs say are currently supported on macOS and Linux. Swiping away, a kill or a power loss is never cancelable and may send no notification at all.

saying these in an interview costs you the question

  • AppLifecycleListener unregisters itself when the widget that created it unmounts.
  • onPause fires on desktop when a window is minimized.
  • onExitRequested can veto the operating system killing a backgrounded phone app.
  • Creating the listener inside build is fine because the old one is replaced.
  • didChangeAppLifecycleState tells you which state the app came from.