In Flutter, how does AppLifecycleListener differ from mixing WidgetsBindingObserver into a State to react to app lifecycle changes?
answer
- an object, not a mixin
- remembers the previous state
- onShow versus onInactive
- onExitRequested returns AppExitResponse
- dispose() removes the observer
basics
~10 sAppLifecycleListener, 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 sWith 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 linesimport '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
Remember that both approaches need cleanup in dispose, and that the listener is the newer, callback-based API.
Explain how the listener derives transitions from the previous state and why onShow and onInactive both land on inactive.
Show when you keep the mixin for metrics or memory pressure, and how you move lifecycle handling out of widgets into a disposable service.
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.