1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport 'package:flutter/services.dart';
6library;
7
8import 'dart:ui';
9import 'package:flutter/foundation.dart';
10
11import 'binding.dart';
12
13/// A callback type that is used by [AppLifecycleListener.onExitRequested] to
14/// ask the application if it wants to cancel application termination or not.
15typedef AppExitRequestCallback = Future<AppExitResponse> Function();
16
17/// A listener that can be used to listen to changes in the application
18/// lifecycle.
19///
20/// To listen for requests for the application to exit, and to decide whether or
21/// not the application should exit when requested, create an
22/// [AppLifecycleListener] and set the [onExitRequested] callback.
23///
24/// To listen for changes in the application lifecycle state, define an
25/// [onStateChange] callback. See the [AppLifecycleState] enum for details on
26/// the various states.
27///
28/// The [onStateChange] callback is called for each state change, and the
29/// individual state transitions ([onResume], [onInactive], etc.) are also
30/// called if the state transition they represent occurs.
31///
32/// State changes will occur in accordance with the state machine described by
33/// this diagram:
34///
35/// ![Diagram of the application lifecycle defined by the AppLifecycleState enum](
36/// https://flutter.github.io/assets-for-api-docs/assets/dart-ui/app_lifecycle.png)
37///
38/// The initial state of the state machine is the [AppLifecycleState.detached]
39/// state, and the arrows describe valid state transitions. Transitions in blue
40/// are transitions that only happen on iOS and Android.
41///
42/// {@tool dartpad}
43/// This example shows how an application can listen to changes in the
44/// application state.
45///
46/// ** See code in examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart **
47/// {@end-tool}
48///
49/// {@tool dartpad}
50/// This example shows how an application can optionally decide to abort a
51/// request for exiting instead of obeying the request.
52///
53/// ** See code in examples/api/lib/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart **
54/// {@end-tool}
55///
56/// See also:
57///
58/// * [ServicesBinding.exitApplication] for a function to call that will request
59/// that the application exits.
60/// * [WidgetsBindingObserver.didRequestAppExit] for the handler which this
61/// class uses to receive exit requests.
62/// * [WidgetsBindingObserver.didChangeAppLifecycleState] for the handler which
63/// this class uses to receive lifecycle state changes.
64class AppLifecycleListener with WidgetsBindingObserver, Diagnosticable {
65 /// Creates an [AppLifecycleListener].
66 AppLifecycleListener({
67 WidgetsBinding? binding,
68 this.onResume,
69 this.onInactive,
70 this.onHide,
71 this.onShow,
72 this.onPause,
73 this.onRestart,
74 this.onDetach,
75 this.onExitRequested,
76 this.onStateChange,
77 }) : binding = binding ?? WidgetsBinding.instance,
78 _lifecycleState = (binding ?? WidgetsBinding.instance).lifecycleState {
79 assert(debugMaybeDispatchCreated('widgets', 'AppLifecycleListener', this));
80 this.binding.addObserver(this);
81 }
82
83 AppLifecycleState? _lifecycleState;
84
85 /// The [WidgetsBinding] to listen to for application lifecycle events.
86 ///
87 /// Typically, this is set to [WidgetsBinding.instance], but may be
88 /// substituted for testing or other specialized bindings.
89 ///
90 /// Defaults to [WidgetsBinding.instance].
91 final WidgetsBinding binding;
92
93 /// Called anytime the state changes, passing the new state.
94 final ValueChanged<AppLifecycleState>? onStateChange;
95
96 /// A callback that is called when the application loses input focus.
97 ///
98 /// On mobile platforms, this can be during a phone call or when a system
99 /// dialog is visible.
100 ///
101 /// On desktop platforms, this is when all views in an application have lost
102 /// input focus but at least one view of the application is still visible.
103 ///
104 /// On the web, this is when the window (or tab) has lost input focus.
105 final VoidCallback? onInactive;
106
107 /// A callback that is called when a view in the application gains input
108 /// focus.
109 ///
110 /// A call to this callback indicates that the application is entering a state
111 /// where it is visible, active, and accepting user input.
112 final VoidCallback? onResume;
113
114 /// A callback that is called when the application is hidden.
115 ///
116 /// On mobile platforms, this is usually just before the application is
117 /// replaced by another application in the foreground.
118 ///
119 /// On desktop platforms, this is just before the application is hidden by
120 /// being minimized or otherwise hiding all views of the application.
121 ///
122 /// On the web, this is just before a window (or tab) is hidden.
123 final VoidCallback? onHide;
124
125 /// A callback that is called when the application is shown.
126 ///
127 /// On mobile platforms, this is usually just before the application replaces
128 /// another application in the foreground.
129 ///
130 /// On desktop platforms, this is just before the application is shown after
131 /// being minimized or otherwise made to show at least one view of the
132 /// application.
133 ///
134 /// On the web, this is just before a window (or tab) is shown.
135 final VoidCallback? onShow;
136
137 /// A callback that is called when the application is paused.
138 ///
139 /// On mobile platforms, this happens right before the application is replaced
140 /// by another application.
141 ///
142 /// On desktop platforms and the web, this function is not called.
143 final VoidCallback? onPause;
144
145 /// A callback that is called when the application is resumed after being
146 /// paused.
147 ///
148 /// On mobile platforms, this happens just before this application takes over
149 /// as the active application.
150 ///
151 /// On desktop platforms and the web, this function is not called.
152 final VoidCallback? onRestart;
153
154 /// A callback used to ask the application if it will allow exiting the
155 /// application for cases where the exit is cancelable.
156 ///
157 /// Exiting the application isn't always cancelable, but when it is, this
158 /// function will be called before exit occurs.
159 ///
160 /// Responding [AppExitResponse.exit] will continue termination, and
161 /// responding [AppExitResponse.cancel] will cancel it. If termination is not
162 /// canceled, the application will immediately exit.
163 final AppExitRequestCallback? onExitRequested;
164
165 /// A callback that is called when an application has exited, and detached all
166 /// host views from the engine.
167 ///
168 /// This callback is only called on iOS and Android.
169 final VoidCallback? onDetach;
170
171 bool _debugDisposed = false;
172
173 /// Call when the listener is no longer in use.
174 ///
175 /// Do not use the object after calling [dispose].
176 ///
177 /// Subclasses must call this method in their overridden [dispose], if any.
178 @mustCallSuper
179 void dispose() {
180 assert(_debugAssertNotDisposed());
181 assert(debugMaybeDispatchDisposed(this));
182 binding.removeObserver(this);
183 assert(() {
184 _debugDisposed = true;
185 return true;
186 }());
187 }
188
189 bool _debugAssertNotDisposed() {
190 assert(() {
191 if (_debugDisposed) {
192 throw FlutterError(
193 'A $runtimeType was used after being disposed.\n'
194 'Once you have called dispose() on a $runtimeType, it '
195 'can no longer be used.',
196 );
197 }
198 return true;
199 }());
200 return true;
201 }
202
203 @override
204 Future<AppExitResponse> didRequestAppExit() async {
205 assert(_debugAssertNotDisposed());
206 if (onExitRequested == null) {
207 return AppExitResponse.exit;
208 }
209 return onExitRequested!();
210 }
211
212 @override
213 void didChangeAppLifecycleState(AppLifecycleState state) {
214 assert(_debugAssertNotDisposed());
215 final AppLifecycleState? previousState = _lifecycleState;
216 if (state == previousState) {
217 // Transitioning to the same state twice doesn't produce any
218 // notifications (but also won't actually occur).
219 return;
220 }
221 _lifecycleState = state;
222 switch (state) {
223 case AppLifecycleState.resumed:
224 assert(
225 previousState == null ||
226 previousState == AppLifecycleState.inactive ||
227 previousState == AppLifecycleState.detached,
228 'Invalid state transition from $previousState to $state',
229 );
230 onResume?.call();
231 case AppLifecycleState.inactive:
232 assert(
233 previousState == null ||
234 previousState == AppLifecycleState.hidden ||
235 previousState == AppLifecycleState.resumed,
236 'Invalid state transition from $previousState to $state',
237 );
238 if (previousState == AppLifecycleState.hidden) {
239 onShow?.call();
240 } else if (previousState == null || previousState == AppLifecycleState.resumed) {
241 onInactive?.call();
242 }
243 case AppLifecycleState.hidden:
244 assert(
245 previousState == null ||
246 previousState == AppLifecycleState.paused ||
247 previousState == AppLifecycleState.inactive,
248 'Invalid state transition from $previousState to $state',
249 );
250 if (previousState == AppLifecycleState.paused) {
251 onRestart?.call();
252 } else if (previousState == null || previousState == AppLifecycleState.inactive) {
253 onHide?.call();
254 }
255 case AppLifecycleState.paused:
256 assert(
257 previousState == null || previousState == AppLifecycleState.hidden,
258 'Invalid state transition from $previousState to $state',
259 );
260 if (previousState == null || previousState == AppLifecycleState.hidden) {
261 onPause?.call();
262 }
263 case AppLifecycleState.detached:
264 assert(
265 previousState == null || previousState == AppLifecycleState.paused,
266 'Invalid state transition from $previousState to $state',
267 );
268 onDetach?.call();
269 }
270 // At this point, it can't be null anymore.
271 onStateChange?.call(_lifecycleState!);
272 }
273
274 @override
275 void debugFillProperties(DiagnosticPropertiesBuilder properties) {
276 super.debugFillProperties(properties);
277 properties.add(DiagnosticsProperty<WidgetsBinding>('binding', binding));
278 properties.add(
279 FlagProperty('onStateChange', value: onStateChange != null, ifTrue: 'onStateChange'),
280 );
281 properties.add(FlagProperty('onInactive', value: onInactive != null, ifTrue: 'onInactive'));
282 properties.add(FlagProperty('onResume', value: onResume != null, ifTrue: 'onResume'));
283 properties.add(FlagProperty('onHide', value: onHide != null, ifTrue: 'onHide'));
284 properties.add(FlagProperty('onShow', value: onShow != null, ifTrue: 'onShow'));
285 properties.add(FlagProperty('onPause', value: onPause != null, ifTrue: 'onPause'));
286 properties.add(FlagProperty('onRestart', value: onRestart != null, ifTrue: 'onRestart'));
287 properties.add(
288 FlagProperty('onExitRequested', value: onExitRequested != null, ifTrue: 'onExitRequested'),
289 );
290 properties.add(FlagProperty('onDetach', value: onDetach != null, ifTrue: 'onDetach'));
291 }
292}
293