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