Add AppLifecycleListener
, with support for application exit handling (#123274)
## Description This adds `AppLifecycleListener`, a class for listening to changes in the application lifecycle, and responding to requests to exit the application. It depends on changes in the Engine that add new lifecycle states: https://github.com/flutter/engine/pull/42418 Here's a diagram for the lifecycle states. I'll add a similar diagram to the documentation for these classes.  ## Related Issues - https://github.com/flutter/flutter/issues/30735 ## Tests - Added tests for new lifecycle value, as well as for the `AppLifecycleListener` itself.
This commit is contained in:
parent
f2351f61d4
commit
a280346193
@ -0,0 +1,100 @@
|
||||
// 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/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
|
||||
/// Flutter code sample for [AppLifecycleListener].
|
||||
|
||||
void main() {
|
||||
runApp(const AppLifecycleListenerExample());
|
||||
}
|
||||
|
||||
class AppLifecycleListenerExample extends StatelessWidget {
|
||||
const AppLifecycleListenerExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(body: AppLifecycleDisplay()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AppLifecycleDisplay extends StatefulWidget {
|
||||
const AppLifecycleDisplay({super.key});
|
||||
|
||||
@override
|
||||
State<AppLifecycleDisplay> createState() => _AppLifecycleDisplayState();
|
||||
}
|
||||
|
||||
class _AppLifecycleDisplayState extends State<AppLifecycleDisplay> {
|
||||
late final AppLifecycleListener _listener;
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
final List<String> _states = <String>[];
|
||||
late AppLifecycleState? _state;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_state = SchedulerBinding.instance.lifecycleState;
|
||||
_listener = AppLifecycleListener(
|
||||
onShow: () => _handleTransition('show'),
|
||||
onResume: () => _handleTransition('resume'),
|
||||
onHide: () => _handleTransition('hide'),
|
||||
onInactive: () => _handleTransition('inactive'),
|
||||
onPause: () => _handleTransition('pause'),
|
||||
onDetach: () => _handleTransition('detach'),
|
||||
onRestart: () => _handleTransition('restart'),
|
||||
// This fires for each state change. Callbacks above fire only for
|
||||
// specific state transitions.
|
||||
onStateChange: _handleStateChange,
|
||||
);
|
||||
if (_state != null) {
|
||||
_states.add(_state!.name);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listener.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleTransition(String name) {
|
||||
setState(() {
|
||||
_states.add(name);
|
||||
});
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
|
||||
void _handleStateChange(AppLifecycleState state) {
|
||||
setState(() {
|
||||
_state = state;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Text('Current State: ${_state ?? 'Not initialized yet'}'),
|
||||
const SizedBox(height: 30),
|
||||
Text('State History:\n ${_states.join('\n ')}'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
// 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 [AppLifecycleListener].
|
||||
|
||||
void main() {
|
||||
runApp(const AppLifecycleListenerExample());
|
||||
}
|
||||
|
||||
class AppLifecycleListenerExample extends StatelessWidget {
|
||||
const AppLifecycleListenerExample({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const MaterialApp(
|
||||
home: Scaffold(body: ApplicationExitControl()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ApplicationExitControl extends StatefulWidget {
|
||||
const ApplicationExitControl({super.key});
|
||||
|
||||
@override
|
||||
State<ApplicationExitControl> createState() => _ApplicationExitControlState();
|
||||
}
|
||||
|
||||
class _ApplicationExitControlState extends State<ApplicationExitControl> {
|
||||
late final AppLifecycleListener _listener;
|
||||
bool _shouldExit = false;
|
||||
String _lastExitResponse = 'No exit requested yet';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_listener = AppLifecycleListener(
|
||||
onExitRequested: _handleExitRequest,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_listener.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _quit() async {
|
||||
final AppExitType exitType = _shouldExit ? AppExitType.required : AppExitType.cancelable;
|
||||
await ServicesBinding.instance.exitApplication(exitType);
|
||||
}
|
||||
|
||||
Future<AppExitResponse> _handleExitRequest() async {
|
||||
final AppExitResponse response = _shouldExit ? AppExitResponse.exit : AppExitResponse.cancel;
|
||||
setState(() {
|
||||
_lastExitResponse = '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('Exit Request: $_lastExitResponse'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
// 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/widgets/app_lifecycle_listener/app_lifecycle_listener.0.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('AppLifecycleListener example', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.AppLifecycleListenerExample(),
|
||||
);
|
||||
|
||||
expect(find.textContaining('Current State:'), findsOneWidget);
|
||||
expect(find.textContaining('State History:'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
// 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/widgets/app_lifecycle_listener/app_lifecycle_listener.1.dart' as example;
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('AppLifecycleListener example', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
const example.AppLifecycleListenerExample(),
|
||||
);
|
||||
|
||||
expect(find.text('Do Not Allow Exit'), findsOneWidget);
|
||||
expect(find.text('Allow Exit'), findsOneWidget);
|
||||
expect(find.text('Quit'), findsOneWidget);
|
||||
expect(find.textContaining('Exit Request:'), findsOneWidget);
|
||||
await tester.tap(find.text('Quit'));
|
||||
await tester.pump();
|
||||
// Responding to the the quit request happens in a Future that we don't have
|
||||
// visibility for, so to avoid a flaky test with a delay, we just check to
|
||||
// see if the request string prefix is still there, rather than the request
|
||||
// response string. Testing it wasn't worth exposing a Completer in the
|
||||
// example code.
|
||||
expect(find.textContaining('Exit Request:'), findsOneWidget);
|
||||
});
|
||||
}
|
@ -33,14 +33,14 @@ void main() {
|
||||
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
await tester.pumpAndSettle();
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('state is: AppLifecycleState.paused'), findsOneWidget);
|
||||
// Can't look for paused text here because rendering is paused.
|
||||
|
||||
await setAppLifeCycleState(AppLifecycleState.detached);
|
||||
await setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('state is: AppLifecycleState.inactive'), findsNWidgets(2));
|
||||
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('state is: AppLifecycleState.detached'), findsOneWidget);
|
||||
expect(find.text('state is: AppLifecycleState.resumed'), findsNWidgets(2));
|
||||
});
|
||||
}
|
||||
|
@ -371,8 +371,9 @@ mixin SchedulerBinding on BindingBase {
|
||||
/// This is set by [handleAppLifecycleStateChanged] when the
|
||||
/// [SystemChannels.lifecycle] notification is dispatched.
|
||||
///
|
||||
/// The preferred way to watch for changes to this value is using
|
||||
/// [WidgetsBindingObserver.didChangeAppLifecycleState].
|
||||
/// The preferred ways to watch for changes to this value are using
|
||||
/// [WidgetsBindingObserver.didChangeAppLifecycleState], or through an
|
||||
/// [AppLifecycleListener] object.
|
||||
AppLifecycleState? get lifecycleState => _lifecycleState;
|
||||
AppLifecycleState? _lifecycleState;
|
||||
|
||||
@ -392,19 +393,18 @@ mixin SchedulerBinding on BindingBase {
|
||||
@protected
|
||||
@mustCallSuper
|
||||
void handleAppLifecycleStateChanged(AppLifecycleState state) {
|
||||
if (lifecycleState == state) {
|
||||
return;
|
||||
}
|
||||
_lifecycleState = state;
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
case AppLifecycleState.inactive:
|
||||
_setFramesEnabledState(true);
|
||||
case AppLifecycleState.hidden:
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
_setFramesEnabledState(false);
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
// TODO(gspencergoog): Remove this and replace with real cases once
|
||||
// engine change rolls into framework.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -243,28 +243,104 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
||||
///
|
||||
/// Once the [lifecycleState] is populated through any means (including this
|
||||
/// method), this method will do nothing. This is because the
|
||||
/// [dart:ui.PlatformDispatcher.initialLifecycleState] may already be
|
||||
/// stale and it no longer makes sense to use the initial state at dart vm
|
||||
/// startup as the current state anymore.
|
||||
/// [dart:ui.PlatformDispatcher.initialLifecycleState] may already be stale
|
||||
/// and it no longer makes sense to use the initial state at dart vm startup
|
||||
/// as the current state anymore.
|
||||
///
|
||||
/// The latest state should be obtained by subscribing to
|
||||
/// [WidgetsBindingObserver.didChangeAppLifecycleState].
|
||||
@protected
|
||||
void readInitialLifecycleStateFromNativeWindow() {
|
||||
if (lifecycleState != null) {
|
||||
if (lifecycleState != null || platformDispatcher.initialLifecycleState.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final AppLifecycleState? state = _parseAppLifecycleMessage(platformDispatcher.initialLifecycleState);
|
||||
if (state != null) {
|
||||
handleAppLifecycleStateChanged(state);
|
||||
}
|
||||
_handleLifecycleMessage(platformDispatcher.initialLifecycleState);
|
||||
}
|
||||
|
||||
Future<String?> _handleLifecycleMessage(String? message) async {
|
||||
handleAppLifecycleStateChanged(_parseAppLifecycleMessage(message!)!);
|
||||
final AppLifecycleState? state = _parseAppLifecycleMessage(message!);
|
||||
final List<AppLifecycleState> generated = _generateStateTransitions(lifecycleState, state!);
|
||||
generated.forEach(handleAppLifecycleStateChanged);
|
||||
return null;
|
||||
}
|
||||
|
||||
List<AppLifecycleState> _generateStateTransitions(AppLifecycleState? previousState, AppLifecycleState state) {
|
||||
if (previousState == state) {
|
||||
return const <AppLifecycleState>[];
|
||||
}
|
||||
if (previousState == AppLifecycleState.paused && state == AppLifecycleState.detached) {
|
||||
// Handle the wrap-around from paused to detached
|
||||
return const <AppLifecycleState>[
|
||||
AppLifecycleState.detached,
|
||||
];
|
||||
}
|
||||
final List<AppLifecycleState> stateChanges = <AppLifecycleState>[];
|
||||
if (previousState == null) {
|
||||
// If there was no previous state, just jump directly to the new state.
|
||||
stateChanges.add(state);
|
||||
} else {
|
||||
final int previousStateIndex = AppLifecycleState.values.indexOf(previousState);
|
||||
final int stateIndex = AppLifecycleState.values.indexOf(state);
|
||||
assert(previousStateIndex != -1, 'State $previousState missing in stateOrder array');
|
||||
assert(stateIndex != -1, 'State $state missing in stateOrder array');
|
||||
if (previousStateIndex > stateIndex) {
|
||||
for (int i = stateIndex; i < previousStateIndex; ++i) {
|
||||
stateChanges.insert(0, AppLifecycleState.values[i]);
|
||||
}
|
||||
} else {
|
||||
for (int i = previousStateIndex + 1; i <= stateIndex; ++i) {
|
||||
stateChanges.add(AppLifecycleState.values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert((){
|
||||
AppLifecycleState? starting = previousState;
|
||||
for (final AppLifecycleState ending in stateChanges) {
|
||||
if (!_debugVerifyLifecycleChange(starting, ending)) {
|
||||
return false;
|
||||
}
|
||||
starting = ending;
|
||||
}
|
||||
return true;
|
||||
}(), 'Invalid lifecycle state transition generated from $previousState to $state (generated $stateChanges)');
|
||||
return stateChanges;
|
||||
}
|
||||
|
||||
static bool _debugVerifyLifecycleChange(AppLifecycleState? starting, AppLifecycleState ending) {
|
||||
if (starting == null) {
|
||||
// Any transition from null is fine, since it is initializing the state.
|
||||
return true;
|
||||
}
|
||||
if (starting == ending) {
|
||||
// Any transition to itself shouldn't happen.
|
||||
return false;
|
||||
}
|
||||
switch (starting) {
|
||||
case AppLifecycleState.detached:
|
||||
if (ending == AppLifecycleState.resumed || ending == AppLifecycleState.paused) {
|
||||
return true;
|
||||
}
|
||||
case AppLifecycleState.resumed:
|
||||
// Can't go from resumed to detached directly (must go through paused).
|
||||
if (ending == AppLifecycleState.inactive) {
|
||||
return true;
|
||||
}
|
||||
case AppLifecycleState.inactive:
|
||||
if (ending == AppLifecycleState.resumed || ending == AppLifecycleState.hidden) {
|
||||
return true;
|
||||
}
|
||||
case AppLifecycleState.hidden:
|
||||
if (ending == AppLifecycleState.inactive || ending == AppLifecycleState.paused) {
|
||||
return true;
|
||||
}
|
||||
case AppLifecycleState.paused:
|
||||
if (ending == AppLifecycleState.hidden || ending == AppLifecycleState.detached) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<dynamic> _handlePlatformMessage(MethodCall methodCall) async {
|
||||
final String method = methodCall.method;
|
||||
assert(method == 'SystemChrome.systemUIChange' || method == 'System.requestAppExit');
|
||||
@ -359,7 +435,6 @@ mixin ServicesBinding on BindingBase, SchedulerBinding {
|
||||
///
|
||||
/// * [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',
|
||||
|
262
packages/flutter/lib/src/widgets/app_lifecycle_listener.dart
Normal file
262
packages/flutter/lib/src/widgets/app_lifecycle_listener.dart
Normal file
@ -0,0 +1,262 @@
|
||||
// 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:
|
||||
///
|
||||
/// 
|
||||
///
|
||||
/// 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'));
|
||||
}
|
||||
}
|
@ -3309,14 +3309,10 @@ class ClipboardStatusNotifier extends ValueNotifier<ClipboardStatus> with Widget
|
||||
update();
|
||||
case AppLifecycleState.detached:
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.hidden:
|
||||
case AppLifecycleState.paused:
|
||||
// Nothing to do.
|
||||
break;
|
||||
// ignore: no_default_cases
|
||||
default:
|
||||
// TODO(gspencergoog): Remove this and replace with real cases once
|
||||
// engine change rolls into framework.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ export 'src/widgets/animated_size.dart';
|
||||
export 'src/widgets/animated_switcher.dart';
|
||||
export 'src/widgets/annotated_region.dart';
|
||||
export 'src/widgets/app.dart';
|
||||
export 'src/widgets/app_lifecycle_listener.dart';
|
||||
export 'src/widgets/async.dart';
|
||||
export 'src/widgets/autocomplete.dart';
|
||||
export 'src/widgets/autofill.dart';
|
||||
|
@ -9,18 +9,16 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('initialLifecycleState is used to init state paused', (WidgetTester tester) async {
|
||||
// The lifecycleState is null initially in tests as there is no
|
||||
// initialLifecycleState.
|
||||
expect(ServicesBinding.instance.lifecycleState, equals(null));
|
||||
// Mock the Window to provide paused as the AppLifecycleState
|
||||
expect(ServicesBinding.instance.lifecycleState, isNull);
|
||||
final TestWidgetsFlutterBinding binding = tester.binding;
|
||||
binding.resetLifecycleState();
|
||||
// Use paused as the initial state.
|
||||
binding.platformDispatcher.initialLifecycleStateTestValue = 'AppLifecycleState.paused';
|
||||
binding.readTestInitialLifecycleStateFromNativeWindow(); // Re-attempt the initialization.
|
||||
|
||||
// The lifecycleState should now be the state we passed above,
|
||||
// even though no lifecycle event was fired from the platform.
|
||||
expect(ServicesBinding.instance.lifecycleState.toString(), equals('AppLifecycleState.paused'));
|
||||
expect(binding.lifecycleState.toString(), equals('AppLifecycleState.paused'));
|
||||
});
|
||||
testWidgets('Handles all of the allowed states of AppLifecycleState', (WidgetTester tester) async {
|
||||
final TestWidgetsFlutterBinding binding = tester.binding;
|
||||
@ -31,4 +29,18 @@ void main() {
|
||||
expect(ServicesBinding.instance.lifecycleState.toString(), equals(state.toString()));
|
||||
}
|
||||
});
|
||||
test('AppLifecycleState values are in the right order for the state machine to be correct', () {
|
||||
expect(
|
||||
AppLifecycleState.values,
|
||||
equals(
|
||||
<AppLifecycleState>[
|
||||
AppLifecycleState.detached,
|
||||
AppLifecycleState.resumed,
|
||||
AppLifecycleState.inactive,
|
||||
AppLifecycleState.hidden,
|
||||
AppLifecycleState.paused,
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
190
packages/flutter/test/widgets/app_lifecycle_listener_test.dart
Normal file
190
packages/flutter/test/widgets/app_lifecycle_listener_test.dart
Normal file
@ -0,0 +1,190 @@
|
||||
// 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/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
late AppLifecycleListener listener;
|
||||
|
||||
Future<void> setAppLifeCycleState(AppLifecycleState state) async {
|
||||
final ByteData? message = const StringCodec().encodeMessage(state.toString());
|
||||
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.handlePlatformMessage('flutter/lifecycle', message, (_) {});
|
||||
}
|
||||
|
||||
Future<void> sendAppExitRequest() async {
|
||||
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('System.requestAppExit'));
|
||||
await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||
.handlePlatformMessage('flutter/platform', message, (_) {});
|
||||
}
|
||||
|
||||
setUp(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
WidgetsBinding.instance
|
||||
..resetEpoch()
|
||||
..platformDispatcher.onBeginFrame = null
|
||||
..platformDispatcher.onDrawFrame = null;
|
||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
|
||||
binding.readTestInitialLifecycleStateFromNativeWindow();
|
||||
// Reset the state to detached. Going to paused first makes it a valid
|
||||
// transition from any state, since the intermediate transitions will be
|
||||
// generated.
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
await setAppLifeCycleState(AppLifecycleState.detached);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
listener.dispose();
|
||||
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.instance;
|
||||
binding.resetLifecycleState();
|
||||
binding.platformDispatcher.resetInitialLifecycleState();
|
||||
assert(TestAppLifecycleListener.registerCount == 0,
|
||||
'There were ${TestAppLifecycleListener.registerCount} listeners that were not disposed of in tests.');
|
||||
});
|
||||
|
||||
testWidgets('Default Diagnostics', (WidgetTester tester) async {
|
||||
listener = TestAppLifecycleListener(binding: tester.binding);
|
||||
expect(listener.toString(),
|
||||
equalsIgnoringHashCodes('TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>)'));
|
||||
});
|
||||
|
||||
testWidgets('Diagnostics', (WidgetTester tester) async {
|
||||
Future<AppExitResponse> handleExitRequested() async {
|
||||
return AppExitResponse.cancel;
|
||||
}
|
||||
|
||||
listener = TestAppLifecycleListener(
|
||||
binding: WidgetsBinding.instance,
|
||||
onExitRequested: handleExitRequested,
|
||||
onStateChange: (AppLifecycleState _) {},
|
||||
);
|
||||
expect(
|
||||
listener.toString(),
|
||||
equalsIgnoringHashCodes(
|
||||
'TestAppLifecycleListener#00000(binding: <AutomatedTestWidgetsFlutterBinding>, onStateChange, onExitRequested)'));
|
||||
});
|
||||
|
||||
testWidgets('listens to AppLifecycleState', (WidgetTester tester) async {
|
||||
final List<AppLifecycleState> states = <AppLifecycleState>[tester.binding.lifecycleState!];
|
||||
void stateChange(AppLifecycleState state) {
|
||||
states.add(state);
|
||||
}
|
||||
|
||||
listener = TestAppLifecycleListener(
|
||||
binding: WidgetsBinding.instance,
|
||||
onStateChange: stateChange,
|
||||
);
|
||||
expect(states, equals(<AppLifecycleState>[AppLifecycleState.detached]));
|
||||
await setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
// "resumed" is generated.
|
||||
expect(states,
|
||||
equals(<AppLifecycleState>[AppLifecycleState.detached, AppLifecycleState.resumed, AppLifecycleState.inactive]));
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(
|
||||
states,
|
||||
equals(<AppLifecycleState>[
|
||||
AppLifecycleState.detached,
|
||||
AppLifecycleState.resumed,
|
||||
AppLifecycleState.inactive,
|
||||
AppLifecycleState.resumed
|
||||
]));
|
||||
});
|
||||
|
||||
testWidgets('Triggers correct state transition callbacks', (WidgetTester tester) async {
|
||||
final List<String> transitions = <String>[];
|
||||
listener = TestAppLifecycleListener(
|
||||
binding: WidgetsBinding.instance,
|
||||
onDetach: () => transitions.add('detach'),
|
||||
onHide: () => transitions.add('hide'),
|
||||
onInactive: () => transitions.add('inactive'),
|
||||
onPause: () => transitions.add('pause'),
|
||||
onRestart: () => transitions.add('restart'),
|
||||
onResume: () => transitions.add('resume'),
|
||||
onShow: () => transitions.add('show'),
|
||||
);
|
||||
|
||||
// Try "standard" sequence
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(transitions, equals(<String>['resume']));
|
||||
await setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
expect(transitions, equals(<String>['resume', 'inactive']));
|
||||
await setAppLifeCycleState(AppLifecycleState.hidden);
|
||||
expect(transitions, equals(<String>['resume', 'inactive', 'hide']));
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(transitions, equals(<String>['resume', 'inactive', 'hide', 'pause']));
|
||||
|
||||
// Go back to resume
|
||||
transitions.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.hidden);
|
||||
expect(transitions, equals(<String>['restart']));
|
||||
await setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
expect(transitions, equals(<String>['restart', 'show']));
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(transitions, equals(<String>['restart', 'show', 'resume']));
|
||||
|
||||
// Generates intermediate states.
|
||||
transitions.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(transitions, equals(<String>['inactive', 'hide', 'pause']));
|
||||
// Wraps around from pause to detach.
|
||||
await setAppLifeCycleState(AppLifecycleState.detached);
|
||||
expect(transitions, equals(<String>['inactive', 'hide', 'pause', 'detach']));
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(transitions, equals(<String>['inactive', 'hide', 'pause', 'detach', 'resume']));
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(transitions, equals(<String>['inactive', 'hide', 'pause', 'detach', 'resume', 'inactive', 'hide', 'pause']));
|
||||
transitions.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(transitions, equals(<String>['restart', 'show', 'resume']));
|
||||
|
||||
// Asserts on bad transitions
|
||||
await expectLater(() => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError);
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
await setAppLifeCycleState(AppLifecycleState.detached);
|
||||
});
|
||||
|
||||
testWidgets('Receives exit requests', (WidgetTester tester) async {
|
||||
bool exitRequested = false;
|
||||
Future<AppExitResponse> handleExitRequested() async {
|
||||
exitRequested = true;
|
||||
return AppExitResponse.cancel;
|
||||
}
|
||||
|
||||
listener = TestAppLifecycleListener(
|
||||
binding: WidgetsBinding.instance,
|
||||
onExitRequested: handleExitRequested,
|
||||
);
|
||||
await sendAppExitRequest();
|
||||
expect(exitRequested, isTrue);
|
||||
});
|
||||
}
|
||||
|
||||
class TestAppLifecycleListener extends AppLifecycleListener {
|
||||
TestAppLifecycleListener({
|
||||
super.binding,
|
||||
super.onResume,
|
||||
super.onInactive,
|
||||
super.onHide,
|
||||
super.onShow,
|
||||
super.onPause,
|
||||
super.onRestart,
|
||||
super.onDetach,
|
||||
super.onExitRequested,
|
||||
super.onStateChange,
|
||||
}) {
|
||||
registerCount += 1;
|
||||
}
|
||||
|
||||
static int registerCount = 0;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
registerCount -= 1;
|
||||
}
|
||||
}
|
@ -17,11 +17,11 @@ class MemoryPressureObserver with WidgetsBindingObserver {
|
||||
}
|
||||
|
||||
class AppLifecycleStateObserver with WidgetsBindingObserver {
|
||||
late AppLifecycleState lifecycleState;
|
||||
List<AppLifecycleState> accumulatedStates = <AppLifecycleState>[];
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
lifecycleState = state;
|
||||
accumulatedStates.add(state);
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,19 +66,58 @@ void main() {
|
||||
final AppLifecycleStateObserver observer = AppLifecycleStateObserver();
|
||||
WidgetsBinding.instance.addObserver(observer);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(observer.lifecycleState, AppLifecycleState.paused);
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[AppLifecycleState.paused]);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(observer.lifecycleState, AppLifecycleState.resumed);
|
||||
observer.accumulatedStates.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[
|
||||
AppLifecycleState.hidden,
|
||||
AppLifecycleState.inactive,
|
||||
AppLifecycleState.resumed,
|
||||
]);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
expect(observer.lifecycleState, AppLifecycleState.inactive);
|
||||
observer.accumulatedStates.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[
|
||||
AppLifecycleState.inactive,
|
||||
AppLifecycleState.hidden,
|
||||
AppLifecycleState.paused,
|
||||
]);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.detached);
|
||||
expect(observer.lifecycleState, AppLifecycleState.detached);
|
||||
observer.accumulatedStates.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[
|
||||
AppLifecycleState.hidden,
|
||||
AppLifecycleState.inactive,
|
||||
]);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
observer.accumulatedStates.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.hidden);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[
|
||||
AppLifecycleState.hidden,
|
||||
]);
|
||||
|
||||
observer.accumulatedStates.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[
|
||||
AppLifecycleState.paused,
|
||||
]);
|
||||
|
||||
observer.accumulatedStates.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.detached);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[
|
||||
AppLifecycleState.detached,
|
||||
]);
|
||||
|
||||
observer.accumulatedStates.clear();
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(observer.accumulatedStates, <AppLifecycleState>[
|
||||
AppLifecycleState.resumed,
|
||||
]);
|
||||
|
||||
observer.accumulatedStates.clear();
|
||||
await expectLater(() async => setAppLifeCycleState(AppLifecycleState.detached), throwsAssertionError);
|
||||
});
|
||||
|
||||
testWidgets('didPushRoute callback', (WidgetTester tester) async {
|
||||
@ -87,7 +126,7 @@ void main() {
|
||||
|
||||
const String testRouteName = 'testRouteName';
|
||||
final ByteData message = const JSONMethodCodec().encodeMethodCall(const MethodCall('pushRoute', testRouteName));
|
||||
await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) { });
|
||||
await tester.binding.defaultBinaryMessenger.handlePlatformMessage('flutter/navigation', message, (_) {});
|
||||
expect(observer.pushedRoute, testRouteName);
|
||||
|
||||
WidgetsBinding.instance.removeObserver(observer);
|
||||
@ -199,26 +238,29 @@ void main() {
|
||||
testWidgets('Application lifecycle affects frame scheduling', (WidgetTester tester) async {
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.paused);
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(tester.binding.hasScheduledFrame, isTrue);
|
||||
await tester.pump();
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
await setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.detached);
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
await setAppLifeCycleState(AppLifecycleState.detached);
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
await setAppLifeCycleState(AppLifecycleState.inactive);
|
||||
expect(tester.binding.hasScheduledFrame, isTrue);
|
||||
await tester.pump();
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
setAppLifeCycleState(AppLifecycleState.paused);
|
||||
await setAppLifeCycleState(AppLifecycleState.paused);
|
||||
expect(tester.binding.hasScheduledFrame, isFalse);
|
||||
|
||||
tester.binding.scheduleFrame();
|
||||
@ -242,7 +284,7 @@ void main() {
|
||||
expect(frameCount, 1);
|
||||
|
||||
// Get the tester back to a resumed state for subsequent tests.
|
||||
setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
await setAppLifeCycleState(AppLifecycleState.resumed);
|
||||
expect(tester.binding.hasScheduledFrame, isTrue);
|
||||
await tester.pump();
|
||||
});
|
||||
|
@ -379,7 +379,7 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
|
||||
@override
|
||||
void initInstances() {
|
||||
// This is intialized here because it's needed for the `super.initInstances`
|
||||
// This is initialized here because it's needed for the `super.initInstances`
|
||||
// call. It can't be handled as a ctor initializer because it's dependent
|
||||
// on `platformDispatcher`. It can't be handled in the ctor itself because
|
||||
// the base class ctor is called first and calls `initInstances`.
|
||||
@ -499,6 +499,17 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<ui.AppExitResponse> exitApplication(ui.AppExitType exitType, [int exitCode = 0]) async {
|
||||
switch (exitType) {
|
||||
case ui.AppExitType.cancelable:
|
||||
// The test framework shouldn't actually exit when requested.
|
||||
return ui.AppExitResponse.cancel;
|
||||
case ui.AppExitType.required:
|
||||
throw FlutterError('Unexpected application exit request while running test');
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-attempts the initialization of the lifecycle state after providing
|
||||
/// test values in [TestWindow.initialLifecycleStateTestValue].
|
||||
void readTestInitialLifecycleStateFromNativeWindow() {
|
||||
@ -936,8 +947,8 @@ abstract class TestWidgetsFlutterBinding extends BindingBase
|
||||
try {
|
||||
treeDump = rootElement?.toDiagnosticsNode() ?? DiagnosticsNode.message('<no tree>');
|
||||
// We try to stringify the tree dump here (though we immediately discard the result) because
|
||||
// we want to make sure that if it can't be serialised, we replace it with a message that
|
||||
// says the tree could not be serialised. Otherwise, the real exception might get obscured
|
||||
// we want to make sure that if it can't be serialized, we replace it with a message that
|
||||
// says the tree could not be serialized. Otherwise, the real exception might get obscured
|
||||
// by side-effects of the underlying issues causing the tree dumping code to flail.
|
||||
treeDump.toStringDeep();
|
||||
} catch (exception) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user