mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Add support for application exit requests (#121378)
Add support for application exit requests
This commit is contained in:
parent
b1464e00ee
commit
bcdab118ba
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -29,6 +29,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
110
examples/api/lib/services/binding/handle_request_app_exit.0.dart
Normal file
110
examples/api/lib/services/binding/handle_request_app_exit.0.dart
Normal file
@ -0,0 +1,110 @@
|
||||
// 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/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Flutter code sample for [ServicesBinding.handleRequestAppExit].
|
||||
|
||||
void main() {
|
||||
runApp(const ApplicationExitExample());
|
||||
}
|
||||
|
||||
class ApplicationExitExample extends StatelessWidget {
|
||||
const ApplicationExitExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(body: Body()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Body extends StatefulWidget {
|
||||
const Body({super.key});
|
||||
|
||||
@override
|
||||
State<Body> createState() => _BodyState();
|
||||
}
|
||||
|
||||
class _BodyState extends State<Body> with WidgetsBindingObserver {
|
||||
bool _shouldExit = false;
|
||||
String lastResponse = 'No exit requested yet';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _quit() async {
|
||||
final AppExitType exitType = _shouldExit ? AppExitType.required : AppExitType.cancelable;
|
||||
setState(() {
|
||||
lastResponse = 'App requesting ${exitType.name} exit';
|
||||
});
|
||||
await ServicesBinding.instance.exitApplication(exitType);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<AppExitResponse> didRequestAppExit() async {
|
||||
final AppExitResponse response = _shouldExit ? AppExitResponse.exit : AppExitResponse.cancel;
|
||||
setState(() {
|
||||
lastResponse = 'App responded ${response.name} to exit request';
|
||||
});
|
||||
return response;
|
||||
}
|
||||
|
||||
void _radioChanged(bool? value) {
|
||||
value ??= true;
|
||||
if (_shouldExit == value) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_shouldExit = value!;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: IntrinsicHeight(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
RadioListTile<bool>(
|
||||
title: const Text('Do Not Allow Exit'),
|
||||
groupValue: _shouldExit,
|
||||
value: false,
|
||||
onChanged: _radioChanged,
|
||||
),
|
||||
RadioListTile<bool>(
|
||||
title: const Text('Allow Exit'),
|
||||
groupValue: _shouldExit,
|
||||
value: true,
|
||||
onChanged: _radioChanged,
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
ElevatedButton(
|
||||
onPressed: _quit,
|
||||
child: const Text('Quit'),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
Text(lastResponse),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -0,0 +1,22 @@
|
||||
// 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 'package:flutter_api_samples/services/binding/handle_request_app_exit.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Application Exit example', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.ApplicationExitExample(),
|
||||
);
|
||||
|
||||
expect(find.text('No exit requested yet'), findsOneWidget);
|
||||
expect(find.text('Do Not Allow Exit'), findsOneWidget);
|
||||
expect(find.text('Allow Exit'), findsOneWidget);
|
||||
expect(find.text('Quit'), findsOneWidget);
|
||||
await tester.tap(find.text('Quit'));
|
||||
await tester.pump();
|
||||
expect(find.text('App requesting cancelable exit'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
@ -263,30 +263,119 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> _handlePlatformMessage(MethodCall methodCall) async {
|
||||
Future<dynamic> _handlePlatformMessage(MethodCall methodCall) async {
|
||||
final String method = methodCall.method;
|
||||
// There is only one incoming method call currently possible.
|
||||
assert(method == 'SystemChrome.systemUIChange');
|
||||
final List<dynamic> args = methodCall.arguments as List<dynamic>;
|
||||
if (_systemUiChangeCallback != null) {
|
||||
await _systemUiChangeCallback!(args[0] as bool);
|
||||
assert(method == 'SystemChrome.systemUIChange' || method == 'System.requestAppExit');
|
||||
switch (method) {
|
||||
case 'SystemChrome.systemUIChange':
|
||||
final List<dynamic> args = methodCall.arguments as List<dynamic>;
|
||||
if (_systemUiChangeCallback != null) {
|
||||
await _systemUiChangeCallback!(args[0] as bool);
|
||||
}
|
||||
break;
|
||||
case 'System.requestAppExit':
|
||||
return <String, dynamic>{'response': (await handleRequestAppExit()).name};
|
||||
}
|
||||
}
|
||||
|
||||
static AppLifecycleState? _parseAppLifecycleMessage(String message) {
|
||||
switch (message) {
|
||||
case 'AppLifecycleState.paused':
|
||||
return AppLifecycleState.paused;
|
||||
case 'AppLifecycleState.resumed':
|
||||
return AppLifecycleState.resumed;
|
||||
case 'AppLifecycleState.inactive':
|
||||
return AppLifecycleState.inactive;
|
||||
case 'AppLifecycleState.paused':
|
||||
return AppLifecycleState.paused;
|
||||
case 'AppLifecycleState.detached':
|
||||
return AppLifecycleState.detached;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Handles any requests for application exit that may be received on the
|
||||
/// [SystemChannels.platform] method channel.
|
||||
///
|
||||
/// By default, returns [ui.AppExitResponse.exit].
|
||||
///
|
||||
/// {@template flutter.services.binding.ServicesBinding.requestAppExit}
|
||||
/// Not all exits are cancelable, so not all exits will call this function. Do
|
||||
/// not rely on this function as a place to save critical data, because you
|
||||
/// will be disappointed. There are a number of ways that the application can
|
||||
/// exit without letting the application know first: power can be unplugged,
|
||||
/// the battery removed, the application can be killed in a task manager or
|
||||
/// command line, or the device could have a rapid unplanned disassembly (i.e.
|
||||
/// it could explode). In all of those cases (and probably others), no
|
||||
/// notification will be given to the application that it is about to exit.
|
||||
/// {@endtemplate}
|
||||
///
|
||||
/// {@tool sample}
|
||||
/// This examples shows how an application can cancel (or not) OS requests for
|
||||
/// quitting an application. Currently this is only supported on macOS and
|
||||
/// Linux.
|
||||
///
|
||||
/// ** See code in examples/api/lib/services/binding/handle_request_app_exit.0.dart **
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WidgetsBindingObserver.didRequestAppExit], which can be overridden to
|
||||
/// respond to this message.
|
||||
/// * [WidgetsBinding.handleRequestAppExit] which overrides this method to
|
||||
/// notify its observers.
|
||||
Future<ui.AppExitResponse> handleRequestAppExit() async {
|
||||
return ui.AppExitResponse.exit;
|
||||
}
|
||||
|
||||
/// Exits the application by calling the native application API method for
|
||||
/// exiting an application cleanly.
|
||||
///
|
||||
/// This differs from calling `dart:io`'s [exit] function in that it gives the
|
||||
/// engine a chance to clean up resources so that it doesn't crash on exit, so
|
||||
/// calling this is always preferred over calling [exit]. It also optionally
|
||||
/// gives handlers of [handleRequestAppExit] a chance to cancel the
|
||||
/// application exit.
|
||||
///
|
||||
/// The [exitType] indicates what kind of exit to perform. For
|
||||
/// [ui.AppExitType.cancelable] exits, the application is queried through a
|
||||
/// call to [handleRequestAppExit], where the application can optionally
|
||||
/// cancel the request for exit. If the [exitType] is
|
||||
/// [ui.AppExitType.required], then the application exits immediately without
|
||||
/// querying the application.
|
||||
///
|
||||
/// For [ui.AppExitType.cancelable] exits, the returned response value is the
|
||||
/// response obtained from the application as to whether the exit was canceled
|
||||
/// or not. Practically, the response will never be [ui.AppExitResponse.exit],
|
||||
/// since the application will have already exited by the time the result
|
||||
/// would have been received.
|
||||
///
|
||||
/// The optional [exitCode] argument will be used as the application exit code
|
||||
/// on platforms where an exit code is supported. On other platforms it may be
|
||||
/// ignored. It defaults to zero.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WidgetsBindingObserver.didRequestAppExit] for a handler you can
|
||||
/// override on a [WidgetsBindingObserver] to receive exit requests.
|
||||
@mustCallSuper
|
||||
Future<ui.AppExitResponse> exitApplication(ui.AppExitType exitType, [int exitCode = 0]) async {
|
||||
final Map<String, Object?>? result = await SystemChannels.platform.invokeMethod<Map<String, Object?>>(
|
||||
'System.exitApplication',
|
||||
<String, Object?>{'type': exitType.name, 'exitCode': exitCode},
|
||||
);
|
||||
if (result == null ) {
|
||||
return ui.AppExitResponse.cancel;
|
||||
}
|
||||
switch (result['response']) {
|
||||
case 'cancel':
|
||||
return ui.AppExitResponse.cancel;
|
||||
case 'exit':
|
||||
default:
|
||||
// In practice, this will never get returned, because the application
|
||||
// will have exited before it returns.
|
||||
return ui.AppExitResponse.exit;
|
||||
}
|
||||
}
|
||||
|
||||
/// The [RestorationManager] synchronizes the restoration data between
|
||||
/// engine and framework.
|
||||
///
|
||||
@ -326,7 +415,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
||||
void setSystemUiChangeCallback(SystemUiChangeCallback? callback) {
|
||||
_systemUiChangeCallback = callback;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Signature for listening to changes in the [SystemUiMode].
|
||||
|
@ -120,6 +120,11 @@ class SystemChannels {
|
||||
/// * `SystemNavigator.pop`: Tells the operating system to close the
|
||||
/// application, or the closest equivalent. See [SystemNavigator.pop].
|
||||
///
|
||||
/// * `System.exitApplication`: Tells the engine to send a request back to
|
||||
/// the application to request an application exit (using
|
||||
/// `System.requestAppExit` below), and if it is not canceled, to terminate
|
||||
/// the application using the platform UI toolkit's termination API.
|
||||
///
|
||||
/// The following incoming methods are defined for this channel (registered
|
||||
/// using [MethodChannel.setMethodCallHandler]):
|
||||
///
|
||||
@ -129,6 +134,9 @@ class SystemChannels {
|
||||
/// [SystemChrome.setSystemUIChangeCallback] to respond to this change in
|
||||
/// application state.
|
||||
///
|
||||
/// * `System.requestAppExit`: The application has requested that it be
|
||||
/// terminated. See [ServicesBinding.exitApplication].
|
||||
///
|
||||
/// Calls to methods that are not implemented on the shell side are ignored
|
||||
/// (so it is safe to call methods when the relevant plugin might be missing).
|
||||
static const MethodChannel platform = OptionalMethodChannel(
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:ui' show AccessibilityFeatures, AppLifecycleState, FrameTiming, Locale, PlatformDispatcher, TimingsCallback;
|
||||
import 'dart:ui' show AccessibilityFeatures, AppExitResponse, AppLifecycleState, FrameTiming, Locale, PlatformDispatcher, TimingsCallback;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -41,7 +41,6 @@ export 'dart:ui' show AppLifecycleState, Locale;
|
||||
/// lifecycle messages. See [didChangeAppLifecycleState].
|
||||
///
|
||||
/// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart **
|
||||
///
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// To respond to other notifications, replace the [didChangeAppLifecycleState]
|
||||
@ -228,6 +227,21 @@ abstract class WidgetsBindingObserver {
|
||||
/// This method exposes notifications from [SystemChannels.lifecycle].
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) { }
|
||||
|
||||
/// Called when a request is received from the system to exit the application.
|
||||
///
|
||||
/// If any observer responds with [AppExitResponse.cancel], it will cancel the
|
||||
/// exit. All observers will be asked before exiting.
|
||||
///
|
||||
/// {@macro flutter.services.binding.ServicesBinding.requestAppExit}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [ServicesBinding.exitApplication] for a function to call that will request
|
||||
/// that the application exits.
|
||||
Future<AppExitResponse> didRequestAppExit() async {
|
||||
return AppExitResponse.exit;
|
||||
}
|
||||
|
||||
/// Called when the system is running low on memory.
|
||||
///
|
||||
/// This method exposes the `memoryPressure` notification from
|
||||
@ -526,6 +540,20 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
||||
/// * [WidgetsBindingObserver], which has an example of using this method.
|
||||
bool removeObserver(WidgetsBindingObserver observer) => _observers.remove(observer);
|
||||
|
||||
@override
|
||||
Future<AppExitResponse> handleRequestAppExit() async {
|
||||
bool didCancel = false;
|
||||
for (final WidgetsBindingObserver observer in _observers) {
|
||||
if ((await observer.didRequestAppExit()) == AppExitResponse.cancel) {
|
||||
didCancel = true;
|
||||
// Don't early return. For the case where someone is just using the
|
||||
// observer to know when exit happens, we want to call all the
|
||||
// observers, even if we already know we're going to cancel.
|
||||
}
|
||||
}
|
||||
return didCancel ? AppExitResponse.cancel : AppExitResponse.exit;
|
||||
}
|
||||
|
||||
@override
|
||||
void handleMetricsChanged() {
|
||||
super.handleMetricsChanged();
|
||||
|
@ -4,6 +4,7 @@
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
@ -113,4 +114,35 @@ void main() {
|
||||
expect(data, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
test('Calling exitApplication sends a method call to the engine', () async {
|
||||
bool sentMessage = false;
|
||||
MethodCall? methodCall;
|
||||
binding.defaultBinaryMessenger.setMockMessageHandler('flutter/platform', (ByteData? message) async {
|
||||
methodCall = const JSONMethodCodec().decodeMethodCall(message);
|
||||
sentMessage = true;
|
||||
return const JSONMethodCodec().encodeSuccessEnvelope(<String, String>{'response': 'cancel'});
|
||||
});
|
||||
final AppExitResponse response = await binding.exitApplication(AppExitType.required);
|
||||
expect(sentMessage, isTrue);
|
||||
expect(methodCall, isNotNull);
|
||||
expect((methodCall!.arguments as Map<String, dynamic>)['type'], equals('required'));
|
||||
expect(response, equals(AppExitResponse.cancel));
|
||||
});
|
||||
|
||||
test('Default handleRequestAppExit returns exit', () async {
|
||||
const MethodCall incomingCall = MethodCall('System.requestAppExit', <dynamic>[<String, dynamic>{'type': 'cancelable'}]);
|
||||
bool receivedReply = false;
|
||||
Map<String, dynamic>? result;
|
||||
await binding.defaultBinaryMessenger.handlePlatformMessage('flutter/platform', const JSONMethodCodec().encodeMethodCall(incomingCall),
|
||||
(ByteData? message) async {
|
||||
result = (const JSONMessageCodec().decodeMessage(message) as List<dynamic>)[0] as Map<String, dynamic>;
|
||||
receivedReply = true;
|
||||
},
|
||||
);
|
||||
|
||||
expect(receivedReply, isTrue);
|
||||
expect(result, isNotNull);
|
||||
expect(result!['response'], equals('exit'));
|
||||
});
|
||||
}
|
||||
|
@ -27,6 +27,6 @@
|
||||
<key>NSMainNibFile</key>
|
||||
<string>MainMenu</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>FlutterApplication</string>
|
||||
<string>NSApplication</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
Loading…
Reference in New Issue
Block a user