blob: 54b918d62b041f084f83a6b2ccc6e00722426b0a [file] [log] [blame] [edit]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'binding.dart';
/// A callback type that is used by [AppLifecycleListener.onExitRequested] to
/// ask the application if it wants to cancel application termination or not.
typedef AppExitRequestCallback = Future<AppExitResponse> Function();
/// A listener that can be used to listen to changes in the application
/// lifecycle.
///
/// To listen for requests for the application to exit, and to decide whether or
/// not the application should exit when requested, create an
/// [AppLifecycleListener] and set the [onExitRequested] callback.
///
/// To listen for changes in the application lifecycle state, define an
/// [onStateChange] callback. See the [AppLifecycleState] enum for details on
/// the various states.
///
/// The [onStateChange] callback is called for each state change, and the
/// individual state transitions ([onResume], [onInactive], etc.) are also
/// called if the state transition they represent occurs.
///
/// State changes will occur in accordance with the state machine described by
/// this diagram:
///
/// ![Diagram of the application lifecycle defined by the AppLifecycleState enum](
/// https://flutter.github.io/assets-for-api-docs/assets/dart-ui/app_lifecycle.png)
///
/// The initial state of the state machine is the [AppLifecycleState.detached]
/// state, and the arrows describe valid state transitions. Transitions in blue
/// are transitions that only happen on iOS and Android.
///
/// {@tool dartpad}
/// This example shows how an application can listen to changes in the
/// application state.
///
/// ** See code in examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart **
/// {@end-tool}
///
/// {@tool dartpad}
/// This example shows how an application can optionally decide to abort a
/// request for exiting instead of obeying the request.
///
/// ** See code in examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart **
/// {@end-tool}
///
/// See also:
///
/// * [ServicesBinding.exitApplication] for a function to call that will request
/// that the application exits.
/// * [WidgetsBindingObserver.didRequestAppExit] for the handler which this
/// class uses to receive exit requests.
/// * [WidgetsBindingObserver.didChangeAppLifecycleState] for the handler which
/// this class uses to receive lifecycle state changes.
class AppLifecycleListener with WidgetsBindingObserver, Diagnosticable {
/// Creates an [AppLifecycleListener].
AppLifecycleListener({
WidgetsBinding? binding,
this.onResume,
this.onInactive,
this.onHide,
this.onShow,
this.onPause,
this.onRestart,
this.onDetach,
this.onExitRequested,
this.onStateChange,
}) : binding = binding ?? WidgetsBinding.instance,
_lifecycleState = (binding ?? WidgetsBinding.instance).lifecycleState {
this.binding.addObserver(this);
}
AppLifecycleState? _lifecycleState;
/// The [WidgetsBinding] to listen to for application lifecycle events.
///
/// Typically, this is set to [WidgetsBinding.instance], but may be
/// substituted for testing or other specialized bindings.
///
/// Defaults to [WidgetsBinding.instance].
final WidgetsBinding binding;
/// Called anytime the state changes, passing the new state.
final ValueChanged<AppLifecycleState>? onStateChange;
/// A callback that is called when the application loses input focus.
///
/// On mobile platforms, this can be during a phone call or when a system
/// dialog is visible.
///
/// On desktop platforms, this is when all views in an application have lost
/// input focus but at least one view of the application is still visible.
///
/// On the web, this is when the window (or tab) has lost input focus.
final VoidCallback? onInactive;
/// A callback that is called when a view in the application gains input
/// focus.
///
/// A call to this callback indicates that the application is entering a state
/// where it is visible, active, and accepting user input.
final VoidCallback? onResume;
/// A callback that is called when the application is hidden.
///
/// On mobile platforms, this is usually just before the application is
/// replaced by another application in the foreground.
///
/// On desktop platforms, this is just before the application is hidden by
/// being minimized or otherwise hiding all views of the application.
///
/// On the web, this is just before a window (or tab) is hidden.
final VoidCallback? onHide;
/// A callback that is called when the application is shown.
///
/// On mobile platforms, this is usually just before the application replaces
/// another application in the foreground.
///
/// On desktop platforms, this is just before the application is shown after
/// being minimized or otherwise made to show at least one view of the
/// application.
///
/// On the web, this is just before a window (or tab) is shown.
final VoidCallback? onShow;
/// A callback that is called when the application is paused.
///
/// On mobile platforms, this happens right before the application is replaced
/// by another application.
///
/// On desktop platforms and the web, this function is not called.
final VoidCallback? onPause;
/// A callback that is called when the application is resumed after being
/// paused.
///
/// On mobile platforms, this happens just before this application takes over
/// as the active application.
///
/// On desktop platforms and the web, this function is not called.
final VoidCallback? onRestart;
/// A callback used to ask the application if it will allow exiting the
/// application for cases where the exit is cancelable.
///
/// Exiting the application isn't always cancelable, but when it is, this
/// function will be called before exit occurs.
///
/// Responding [AppExitResponse.exit] will continue termination, and
/// responding [AppExitResponse.cancel] will cancel it. If termination is not
/// canceled, the application will immediately exit.
final AppExitRequestCallback? onExitRequested;
/// A callback that is called when an application has exited, and detached all
/// host views from the engine.
///
/// This callback is only called on iOS and Android.
final VoidCallback? onDetach;
bool _debugDisposed = false;
/// Call when the listener is no longer in use.
///
/// Do not use the object after calling [dispose].
///
/// Subclasses must call this method in their overridden [dispose], if any.
@mustCallSuper
void dispose() {
assert(_debugAssertNotDisposed());
binding.removeObserver(this);
assert(() {
_debugDisposed = true;
return true;
}());
}
bool _debugAssertNotDisposed() {
assert(() {
if (_debugDisposed) {
throw FlutterError(
'A $runtimeType was used after being disposed.\n'
'Once you have called dispose() on a $runtimeType, it '
'can no longer be used.',
);
}
return true;
}());
return true;
}
@override
Future<AppExitResponse> didRequestAppExit() async {
assert(_debugAssertNotDisposed());
if (onExitRequested == null) {
return AppExitResponse.exit;
}
return onExitRequested!();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
assert(_debugAssertNotDisposed());
final AppLifecycleState? previousState = _lifecycleState;
if (state == previousState) {
// Transitioning to the same state twice doesn't produce any
// notifications (but also won't actually occur).
return;
}
_lifecycleState = state;
switch (state) {
case AppLifecycleState.resumed:
assert(previousState == null || previousState == AppLifecycleState.inactive || previousState == AppLifecycleState.detached, 'Invalid state transition from $previousState to $state');
onResume?.call();
case AppLifecycleState.inactive:
assert(previousState == null || previousState == AppLifecycleState.hidden || previousState == AppLifecycleState.resumed, 'Invalid state transition from $previousState to $state');
if (previousState == AppLifecycleState.hidden) {
onShow?.call();
} else if (previousState == null || previousState == AppLifecycleState.resumed) {
onInactive?.call();
}
case AppLifecycleState.hidden:
assert(previousState == null || previousState == AppLifecycleState.paused || previousState == AppLifecycleState.inactive, 'Invalid state transition from $previousState to $state');
if (previousState == AppLifecycleState.paused) {
onRestart?.call();
} else if (previousState == null || previousState == AppLifecycleState.inactive) {
onHide?.call();
}
case AppLifecycleState.paused:
assert(previousState == null || previousState == AppLifecycleState.hidden, 'Invalid state transition from $previousState to $state');
if (previousState == null || previousState == AppLifecycleState.hidden) {
onPause?.call();
}
case AppLifecycleState.detached:
assert(previousState == null || previousState == AppLifecycleState.paused, 'Invalid state transition from $previousState to $state');
onDetach?.call();
}
// At this point, it can't be null anymore.
onStateChange?.call(_lifecycleState!);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<WidgetsBinding>('binding', binding));
properties.add(FlagProperty('onStateChange', value: onStateChange != null, ifTrue: 'onStateChange'));
properties.add(FlagProperty('onInactive', value: onInactive != null, ifTrue: 'onInactive'));
properties.add(FlagProperty('onResume', value: onResume != null, ifTrue: 'onResume'));
properties.add(FlagProperty('onHide', value: onHide != null, ifTrue: 'onHide'));
properties.add(FlagProperty('onShow', value: onShow != null, ifTrue: 'onShow'));
properties.add(FlagProperty('onPause', value: onPause != null, ifTrue: 'onPause'));
properties.add(FlagProperty('onRestart', value: onRestart != null, ifTrue: 'onRestart'));
properties.add(FlagProperty('onExitRequested', value: onExitRequested != null, ifTrue: 'onExitRequested'));
properties.add(FlagProperty('onDetach', value: onDetach != null, ifTrue: 'onDetach'));
}
}