| // 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')); |
| } |
| } |