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 | import 'dart:ui'; |
6 | import 'package:flutter/foundation.dart'; |
7 | |
8 | import '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. |
12 | typedef 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. |
61 | class 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 | |