Unverified Commit a2803461 authored by Greg Spencer's avatar Greg Spencer Committed by GitHub

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.

![Application Lifecycle Diagram](https://github.com/flutter/flutter/assets/8867023/f6937002-cb93-4ab9-a221-25de2c45cf0e)

## Related Issues
 - https://github.com/flutter/flutter/issues/30735

## Tests
- Added tests for new lifecycle value, as well as for the `AppLifecycleListener` itself.
parent f2351f61
// 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 ')}'),
],
),
),
),
);
}
}
// 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'),
],
),
),
),
);
}
}
// 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);
});
}
// 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',
......
This diff is collapsed.
......@@ -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,
],
),
);
});
}
// 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);
setAppLifeCycleState(AppLifecycleState.resumed);
expect(observer.lifecycleState, AppLifecycleState.resumed);
setAppLifeCycleState(AppLifecycleState.inactive);
expect(observer.lifecycleState, AppLifecycleState.inactive);
setAppLifeCycleState(AppLifecycleState.detached);
expect(observer.lifecycleState, AppLifecycleState.detached);
setAppLifeCycleState(AppLifecycleState.resumed);
await setAppLifeCycleState(AppLifecycleState.paused);
expect(observer.accumulatedStates, <AppLifecycleState>[AppLifecycleState.paused]);
observer.accumulatedStates.clear();
await setAppLifeCycleState(AppLifecycleState.resumed);
expect(observer.accumulatedStates, <AppLifecycleState>[
AppLifecycleState.hidden,
AppLifecycleState.inactive,
AppLifecycleState.resumed,
]);
observer.accumulatedStates.clear();
await setAppLifeCycleState(AppLifecycleState.paused);
expect(observer.accumulatedStates, <AppLifecycleState>[
AppLifecycleState.inactive,
AppLifecycleState.hidden,
AppLifecycleState.paused,
]);
observer.accumulatedStates.clear();
await setAppLifeCycleState(AppLifecycleState.inactive);
expect(observer.accumulatedStates, <AppLifecycleState>[
AppLifecycleState.hidden,
AppLifecycleState.inactive,
]);
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);
await setAppLifeCycleState(AppLifecycleState.paused);
expect(tester.binding.hasScheduledFrame, isFalse);
setAppLifeCycleState(AppLifecycleState.detached);
await setAppLifeCycleState(AppLifecycleState.detached);
expect(tester.binding.hasScheduledFrame, isFalse);
setAppLifeCycleState(AppLifecycleState.inactive);
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) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment