mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
Support for vetoing an attempt to pop the current route (#7488)
This commit is contained in:
parent
17bc188803
commit
0ce9917fb2
|
@ -2,6 +2,8 @@
|
|||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TextFieldDemo extends StatefulWidget {
|
||||
|
@ -31,6 +33,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||
}
|
||||
|
||||
bool _autovalidate = false;
|
||||
bool _formWasEdited = false;
|
||||
GlobalKey<FormState> _formKey = new GlobalKey<FormState>();
|
||||
GlobalKey<FormFieldState<InputValue>> _passwordFieldKey = new GlobalKey<FormFieldState<InputValue>>();
|
||||
void _handleSubmitted() {
|
||||
|
@ -45,6 +48,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||
}
|
||||
|
||||
String _validateName(InputValue value) {
|
||||
_formWasEdited = true;
|
||||
if (value.text.isEmpty)
|
||||
return 'Name is required.';
|
||||
RegExp nameExp = new RegExp(r'^[A-za-z ]+$');
|
||||
|
@ -54,6 +58,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||
}
|
||||
|
||||
String _validatePhoneNumber(InputValue value) {
|
||||
_formWasEdited = true;
|
||||
RegExp phoneExp = new RegExp(r'^\d\d\d-\d\d\d\-\d\d\d\d$');
|
||||
if (!phoneExp.hasMatch(value.text))
|
||||
return '###-###-#### - Please enter a valid phone number.';
|
||||
|
@ -61,6 +66,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||
}
|
||||
|
||||
String _validatePassword(InputValue value) {
|
||||
_formWasEdited = true;
|
||||
FormFieldState<InputValue> passwordField = _passwordFieldKey.currentState;
|
||||
if (passwordField.value == null || passwordField.value.text.isEmpty)
|
||||
return 'Please choose a password.';
|
||||
|
@ -69,6 +75,30 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||
return null;
|
||||
}
|
||||
|
||||
Future<bool> _warnUserAboutInvalidData() {
|
||||
final FormState form = _formKey.currentState;
|
||||
if (!_formWasEdited || form.validate())
|
||||
return new Future<bool>.value(true);
|
||||
|
||||
return showDialog/*<bool>*/(
|
||||
context: context,
|
||||
child: new AlertDialog(
|
||||
title: new Text('This form has errors'),
|
||||
content: new Text('Really leave this form?'),
|
||||
actions: <Widget> [
|
||||
new FlatButton(
|
||||
child: new Text('YES'),
|
||||
onPressed: () { Navigator.of(context).pop(true); },
|
||||
),
|
||||
new FlatButton(
|
||||
child: new Text('NO'),
|
||||
onPressed: () { Navigator.of(context).pop(false); },
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
|
@ -79,6 +109,7 @@ class TextFieldDemoState extends State<TextFieldDemo> {
|
|||
body: new Form(
|
||||
key: _formKey,
|
||||
autovalidate: _autovalidate,
|
||||
onWillPop: _warnUserAboutInvalidData,
|
||||
child: new Block(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
children: <Widget>[
|
||||
|
|
|
@ -44,6 +44,7 @@ Future<Null> smokeDemo(WidgetTester tester, String routeName) async {
|
|||
expect(backButton, findsOneWidget);
|
||||
await tester.tap(backButton);
|
||||
await tester.pump(); // Start the pop "back" operation.
|
||||
await tester.pump(); // Complete the willPop() Future.
|
||||
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,9 @@ void main() {
|
|||
expect(backButton, findsOneWidget);
|
||||
await tester.tap(backButton);
|
||||
await tester.pump(); // Start the pop "back" operation.
|
||||
await tester.pump(); // Complete the willPop() Future.
|
||||
await tester.pump(const Duration(seconds: 1)); // transition is complete
|
||||
//await tester.pumpUntilNoTransientCallbacks(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('UPDATE'), findsNothing);
|
||||
});
|
||||
|
|
|
@ -200,8 +200,21 @@ class MaterialPageRoute<T> extends PageRoute<T> {
|
|||
|
||||
_CupertinoBackGestureController _backGestureController;
|
||||
|
||||
/// Support for dismissing this route with a horizontal swipe is enabled
|
||||
/// for [TargetPlatform.iOS]. If attempts to dismiss this route might be
|
||||
/// vetoed because a [WillPopCallback] was defined for the route then the
|
||||
/// platform-specific back gesture is disabled.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [hasScopedWillPopCallback], which is true if a `willPop` callback
|
||||
/// is defined for this route.
|
||||
@override
|
||||
NavigationGestureController startPopGesture(NavigatorState navigator) {
|
||||
// If attempts to dismiss this route might be vetoed, then do not
|
||||
// allow the user to dismiss the route with a swipe.
|
||||
if (hasScopedWillPopCallback)
|
||||
return null;
|
||||
if (controller.status != AnimationStatus.completed)
|
||||
return null;
|
||||
assert(_backGestureController == null);
|
||||
|
|
|
@ -770,6 +770,11 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
|
||||
bool _shouldShowBackArrow;
|
||||
|
||||
Future<Null> _back() async {
|
||||
if (await Navigator.willPop(context) && mounted)
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
Widget _getModifiedAppBar({ EdgeInsets padding, int elevation}) {
|
||||
AppBar appBar = config.appBar;
|
||||
if (appBar == null)
|
||||
|
@ -800,7 +805,7 @@ class ScaffoldState extends State<Scaffold> with TickerProviderStateMixin {
|
|||
leading = new IconButton(
|
||||
icon: new Icon(backIcon),
|
||||
alignment: FractionalOffset.centerLeft,
|
||||
onPressed: () => Navigator.pop(context),
|
||||
onPressed: _back,
|
||||
tooltip: 'Back' // TODO(ianh): Figure out how to localize this string
|
||||
);
|
||||
}
|
||||
|
|
|
@ -141,12 +141,15 @@ class _WidgetsAppState extends State<WidgetsApp> implements WidgetsBindingObserv
|
|||
super.dispose();
|
||||
}
|
||||
|
||||
// On Android: the user has pressed the back button.
|
||||
@override
|
||||
bool didPopRoute() {
|
||||
Future<bool> didPopRoute() async {
|
||||
assert(mounted);
|
||||
NavigatorState navigator = _navigator.currentState;
|
||||
assert(navigator != null);
|
||||
return navigator.pop();
|
||||
if (!await navigator.willPop())
|
||||
return true;
|
||||
return mounted && navigator.pop();
|
||||
}
|
||||
|
||||
@override
|
||||
|
|
|
@ -34,7 +34,7 @@ abstract class WidgetsBindingObserver {
|
|||
/// box, and false otherwise. The [WidgetsApp] widget uses this
|
||||
/// mechanism to notify the [Navigator] widget that it should pop
|
||||
/// its current route if possible.
|
||||
bool didPopRoute() => false;
|
||||
Future<bool> didPopRoute() => new Future<bool>.value(false);
|
||||
|
||||
/// Called when the application's dimensions change. For example,
|
||||
/// when a phone is rotated.
|
||||
|
@ -158,9 +158,9 @@ abstract class WidgetsBinding extends BindingBase implements GestureBinding, Ren
|
|||
/// [WidgetsApp] uses this in conjunction with a [Navigator] to
|
||||
/// cause the back button to close dialog boxes, return from modal
|
||||
/// pages, and so forth.
|
||||
void handlePopRoute() {
|
||||
for (WidgetsBindingObserver observer in _observers) {
|
||||
if (observer.didPopRoute())
|
||||
Future<Null> handlePopRoute() async {
|
||||
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
|
||||
if (await observer.didPopRoute())
|
||||
return;
|
||||
}
|
||||
SystemNavigator.pop();
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'framework.dart';
|
||||
import 'routes.dart';
|
||||
|
||||
/// An optional container for grouping together multiple form field widgets
|
||||
/// (e.g. [Input] widgets).
|
||||
|
@ -23,6 +24,7 @@ class Form extends StatefulWidget {
|
|||
Key key,
|
||||
@required this.child,
|
||||
this.autovalidate: false,
|
||||
this.onWillPop,
|
||||
}) : super(key: key) {
|
||||
assert(child != null);
|
||||
}
|
||||
|
@ -48,6 +50,13 @@ class Form extends StatefulWidget {
|
|||
/// [FormState.validate] to validate.
|
||||
final bool autovalidate;
|
||||
|
||||
/// Enables the form to veto attempts by the user to dismiss the [ModalRoute]
|
||||
/// that contains the form.
|
||||
///
|
||||
/// If the callback returns a Future that resolves to false, the form's route
|
||||
/// will not be popped.
|
||||
WillPopCallback onWillPop;
|
||||
|
||||
@override
|
||||
FormState createState() => new FormState();
|
||||
}
|
||||
|
@ -56,8 +65,30 @@ class FormState extends State<Form> {
|
|||
int _generation = 0;
|
||||
Set<FormFieldState<dynamic>> _fields = new Set<FormFieldState<dynamic>>();
|
||||
|
||||
/// Called when a form field has changed. This will cause all form fields
|
||||
/// to rebuild, useful if form fields have interdependencies.
|
||||
@override
|
||||
void dependenciesChanged() {
|
||||
super.dependenciesChanged();
|
||||
final ModalRoute<dynamic> route = ModalRoute.of(context);
|
||||
if (route != null && config.onWillPop != null) {
|
||||
// Avoid adding our callback twice by removing it first.
|
||||
route.removeScopedWillPopCallback(config.onWillPop);
|
||||
route.addScopedWillPopCallback(config.onWillPop);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateConfig(Form oldConfig) {
|
||||
final ModalRoute<dynamic> route = ModalRoute.of(context);
|
||||
if (config.onWillPop != oldConfig.onWillPop && route != null) {
|
||||
if (oldConfig.onWillPop != null)
|
||||
route.removeScopedWillPopCallback(oldConfig.onWillPop);
|
||||
if (config.onWillPop != null)
|
||||
route.addScopedWillPopCallback(config.onWillPop);
|
||||
}
|
||||
}
|
||||
|
||||
// Called when a form field has changed. This will cause all form fields
|
||||
// to rebuild, useful if form fields have interdependencies.
|
||||
void _fieldDidChange() {
|
||||
setState(() {
|
||||
++_generation;
|
||||
|
|
|
@ -66,6 +66,15 @@ abstract class Route<T> {
|
|||
@mustCallSuper
|
||||
void didReplace(Route<dynamic> oldRoute) { }
|
||||
|
||||
|
||||
/// Returns false if this route wants to veto a [Navigator.pop]. This method is
|
||||
/// called by [Naviagtor.willPop].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Form], which provides an `onWillPop` callback that uses this mechanism.
|
||||
Future<bool> willPop() async => true;
|
||||
|
||||
/// A request was made to pop this route. If the route can handle it
|
||||
/// internally (e.g. because it has its own stack of internal state) then
|
||||
/// return false, otherwise return true. Returning false will prevent the
|
||||
|
@ -109,6 +118,11 @@ abstract class Route<T> {
|
|||
/// back gesture), this should return a controller object that can be used to
|
||||
/// control the transition animation's progress. Otherwise, it should return
|
||||
/// null.
|
||||
///
|
||||
/// If attempts to dismiss this route might be vetoed, for example because
|
||||
/// a [WillPopCallback] was defined for the route, then it may make sense
|
||||
/// to disable the pop gesture. For example, the iOS back gesture is disabled
|
||||
/// when [ModalRoute.hasScopedWillCallback] is true.
|
||||
NavigationGestureController startPopGesture(NavigatorState navigator) {
|
||||
return null;
|
||||
}
|
||||
|
@ -461,6 +475,20 @@ class Navigator extends StatefulWidget {
|
|||
return Navigator.of(context).push(route);
|
||||
}
|
||||
|
||||
/// Returns the value of the current route's `willPop` method. This method is
|
||||
/// typically called before a user-initiated [pop]. For example on Android it's
|
||||
/// called by the binding for the system's back button.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Form], which provides an `onWillPop` callback that enables the form
|
||||
/// to veto a [pop] initiated by the app's back button.
|
||||
/// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used
|
||||
/// to define the route's `willPop` method.
|
||||
static Future<bool> willPop(BuildContext context) {
|
||||
return Navigator.of(context).willPop();
|
||||
}
|
||||
|
||||
/// Pop a route off the navigator that most tightly encloses the given context.
|
||||
///
|
||||
/// Tries to removes the current route, calling its didPop() method. If that
|
||||
|
@ -743,6 +771,22 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
|
|||
assert(() { _debugLocked = false; return true; });
|
||||
}
|
||||
|
||||
/// Returns the value of the current route's `willPop` method. This method is
|
||||
/// typically called before a user-initiated [pop]. For example on Android it's
|
||||
/// called by the binding for the system's back button.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Form], which provides an `onWillPop` callback that enables the form
|
||||
/// to veto a [pop] initiated by the app's back button.
|
||||
/// * [ModalRoute], which has as a `willPop` method that can be defined
|
||||
/// by a list of [WillPopCallback]s.
|
||||
Future<bool> willPop() async {
|
||||
final Route<dynamic> route = _history.last;
|
||||
assert(route._navigator == this);
|
||||
return route.willPop();
|
||||
}
|
||||
|
||||
/// Removes the top route in the [Navigator]'s history.
|
||||
///
|
||||
/// If an argument is provided, that argument will be the return value of the
|
||||
|
|
|
@ -363,6 +363,12 @@ class _ModalScopeStatus extends InheritedWidget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Signature for a callback that verifies that it's OK to call [Navigator.pop].
|
||||
///
|
||||
/// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback], and
|
||||
/// [ModalRoute.removeScopedWillPopCallback].
|
||||
typedef Future<bool> WillPopCallback();
|
||||
|
||||
class _ModalScope extends StatefulWidget {
|
||||
_ModalScope({
|
||||
Key key,
|
||||
|
@ -378,6 +384,9 @@ class _ModalScope extends StatefulWidget {
|
|||
}
|
||||
|
||||
class _ModalScopeState extends State<_ModalScope> {
|
||||
// See addScopedWillPopCallback, removeScopedWillPopCallback in ModalRoute.
|
||||
List<WillPopCallback> willPopCallbacks = <WillPopCallback>[];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
@ -394,9 +403,20 @@ class _ModalScopeState extends State<_ModalScope> {
|
|||
void dispose() {
|
||||
config.route.animation?.removeStatusListener(_animationStatusChanged);
|
||||
config.route.forwardAnimation?.removeStatusListener(_animationStatusChanged);
|
||||
willPopCallbacks = null;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void addWillPopCallback(WillPopCallback callback) {
|
||||
assert(mounted);
|
||||
willPopCallbacks.add(callback);
|
||||
}
|
||||
|
||||
void removeWillPopCallback(WillPopCallback callback) {
|
||||
assert(mounted);
|
||||
willPopCallbacks.remove(callback);
|
||||
}
|
||||
|
||||
void _animationStatusChanged(AnimationStatus status) {
|
||||
setState(() {
|
||||
// The animation's states are our build state, and they changed already.
|
||||
|
@ -618,6 +638,80 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||
Animation<double> get forwardAnimation => _forwardAnimationProxy;
|
||||
ProxyAnimation _forwardAnimationProxy;
|
||||
|
||||
/// Return the value of the first callback added with
|
||||
/// [addScopedWillPopCallback] that returns false. Otherwise return true.
|
||||
///
|
||||
/// Typically this method is not overridden because applications usually
|
||||
/// don't create modal routes directly, they use higher level primitives
|
||||
/// like [showDialog]. The scoped [WillPopCallback] list makes it possible
|
||||
/// for ModalRoute descendants to collectively define the value of `willPop`.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Form], which provides an `onWillPop` callback that uses this mechanism.
|
||||
/// * [addScopedWillPopCallback], which adds a callback to the list this
|
||||
/// method checks.
|
||||
/// * [removeScopedWillPopCallback], which removes a callback from the list
|
||||
/// this method checks.
|
||||
@override
|
||||
Future<bool> willPop() async {
|
||||
final _ModalScopeState scope = _scopeKey.currentState;
|
||||
assert(scope != null);
|
||||
for (WillPopCallback callback in new List<WillPopCallback>.from(scope.willPopCallbacks)) {
|
||||
if (!await callback())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Enables this route to veto attempts by the user to dismiss it.
|
||||
///
|
||||
/// This callback is typically added by a stateful descendant of the modal route.
|
||||
/// A stateful widget shown in a modal route, like the child passed to
|
||||
/// [showDialog], can look up its modal route and then add a callback in its
|
||||
/// `dependenciesChanged` method:
|
||||
///
|
||||
/// ```dart
|
||||
/// @override
|
||||
/// void dependenciesChanged() {
|
||||
/// super.dependenciesChanged();
|
||||
/// ModalRoute.of(context).addScopedWillPopCallback(askTheUserIfTheyAreSure);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// A typical application of this callback would be to warn the user about
|
||||
/// unsaved [Form] data if the user attempts to back out of the form.
|
||||
///
|
||||
/// This callback runs asynchronously and it's possible that it will be called
|
||||
/// after its route has been disposed. The callback should check [mounted] before
|
||||
/// doing anything.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Form], which provides an `onWillPop` callback that uses this mechanism.
|
||||
/// * [willPop], which runs the callbacks added with this method.
|
||||
/// * [removeScopedWillPopCallback], which removes a callback from the list
|
||||
/// that [willPop] checks.
|
||||
void addScopedWillPopCallback(WillPopCallback callback) {
|
||||
assert(_scopeKey.currentState != null);
|
||||
_scopeKey.currentState.addWillPopCallback(callback);
|
||||
}
|
||||
|
||||
/// Remove one of the callbacks run by [willPop].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [Form], which provides an `onWillPop` callback that uses this mechanism.
|
||||
/// * [addScopedWillPopCallback], which adds callback to the list
|
||||
/// checked by [willPop].
|
||||
void removeScopedWillPopCallback(WillPopCallback callback) {
|
||||
assert(_scopeKey.currentState != null);
|
||||
_scopeKey.currentState.removeWillPopCallback(callback);
|
||||
}
|
||||
|
||||
bool get hasScopedWillPopCallback {
|
||||
return _scopeKey.currentState == null || _scopeKey.currentState.willPopCallbacks.length > 0;
|
||||
}
|
||||
|
||||
// Internals
|
||||
|
||||
|
|
246
packages/flutter/test/material/will_pop_test.dart
Normal file
246
packages/flutter/test/material/will_pop_test.dart
Normal file
|
@ -0,0 +1,246 @@
|
|||
// Copyright 2016 The Chromium 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_test/flutter_test.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
bool willPopValue = false;
|
||||
|
||||
class SamplePage extends StatefulWidget {
|
||||
@override
|
||||
SamplePageState createState() => new SamplePageState();
|
||||
}
|
||||
|
||||
class SamplePageState extends State<SamplePage> {
|
||||
@override
|
||||
void dependenciesChanged() {
|
||||
super.dependenciesChanged();
|
||||
final ModalRoute<Null> route = ModalRoute.of(context);
|
||||
if (route.isCurrent)
|
||||
route.addScopedWillPopCallback(() async => willPopValue);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(title: new Text('Sample Page')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
int willPopCount = 0;
|
||||
|
||||
class SampleForm extends StatelessWidget {
|
||||
SampleForm({ Key key, this.callback }) : super(key: key);
|
||||
|
||||
final WillPopCallback callback;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return new Scaffold(
|
||||
appBar: new AppBar(title: new Text('Sample Form')),
|
||||
body: new SizedBox.expand(
|
||||
child: new Form(
|
||||
onWillPop: () {
|
||||
willPopCount += 1;
|
||||
return callback();
|
||||
},
|
||||
child: new InputFormField(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
testWidgets('ModalRoute scopedWillPopupCallback can inhibit back button', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
new MaterialApp(
|
||||
home: new Scaffold(
|
||||
appBar: new AppBar(title: new Text('Home')),
|
||||
body: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new FlatButton(
|
||||
child: new Text('X'),
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
child: new SamplePage(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('Sample Page'), findsOneWidget);
|
||||
|
||||
willPopValue = false;
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.text('Sample Page'), findsOneWidget);
|
||||
|
||||
willPopValue = true;
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pump();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
expect(find.text('Sample Page'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Form.willPop can inhibit back button', (WidgetTester tester) async {
|
||||
Widget buildFrame() {
|
||||
return new MaterialApp(
|
||||
home: new Scaffold(
|
||||
appBar: new AppBar(title: new Text('Home')),
|
||||
body: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new FlatButton(
|
||||
child: new Text('X'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(new MaterialPageRoute<Null>(
|
||||
builder: (BuildContext context) {
|
||||
return new SampleForm(
|
||||
callback: () => new Future<bool>.value(willPopValue),
|
||||
);
|
||||
}
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildFrame());
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('Sample Form'), findsOneWidget);
|
||||
|
||||
willPopValue = false;
|
||||
willPopCount = 0;
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pump(); // Start the pop "back" operation.
|
||||
await tester.pump(); // Complete the willPop() Future.
|
||||
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
|
||||
expect(find.text('Sample Form'), findsOneWidget);
|
||||
expect(willPopCount, 1);
|
||||
|
||||
willPopValue = true;
|
||||
willPopCount = 0;
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pump(); // Start the pop "back" operation.
|
||||
await tester.pump(); // Complete the willPop() Future.
|
||||
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
|
||||
expect(find.text('Sample Form'), findsNothing);
|
||||
expect(willPopCount, 1);
|
||||
});
|
||||
|
||||
testWidgets('Form.willPop callbacks do not accumulate', (WidgetTester tester) async {
|
||||
Future<bool> showYesNoAlert(BuildContext context) {
|
||||
return showDialog/*<bool>*/(
|
||||
context: context,
|
||||
child: new AlertDialog(
|
||||
actions: <Widget> [
|
||||
new FlatButton(
|
||||
child: new Text('YES'),
|
||||
onPressed: () { Navigator.of(context).pop(true); },
|
||||
),
|
||||
new FlatButton(
|
||||
child: new Text('NO'),
|
||||
onPressed: () { Navigator.of(context).pop(false); },
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildFrame() {
|
||||
return new MaterialApp(
|
||||
home: new Scaffold(
|
||||
appBar: new AppBar(title: new Text('Home')),
|
||||
body: new Builder(
|
||||
builder: (BuildContext context) {
|
||||
return new Center(
|
||||
child: new FlatButton(
|
||||
child: new Text('X'),
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(new MaterialPageRoute<Null>(
|
||||
builder: (BuildContext context) {
|
||||
return new SampleForm(
|
||||
callback: () => showYesNoAlert(context),
|
||||
);
|
||||
}
|
||||
));
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpWidget(buildFrame());
|
||||
|
||||
await tester.tap(find.text('X'));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(seconds: 1));
|
||||
|
||||
expect(find.text('Sample Form'), findsOneWidget);
|
||||
|
||||
// Press the Scaffold's back button. This causes the willPop callback
|
||||
// to run, which shows the YES/NO Alert Dialog. Veto the back operation
|
||||
// by pressing the Alert's NO button.
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pump(); // Start the pop "back" operation.
|
||||
await tester.pump(); // Call willPop which will show an Alert.
|
||||
await tester.tap(find.text('NO'));
|
||||
await tester.pump(); // Start the dismiss animation.
|
||||
await tester.pump(); // Resolve the willPop callback.
|
||||
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
|
||||
expect(find.text('Sample Form'), findsOneWidget);
|
||||
|
||||
// Do it again. Note that each time the Alert is shown and dismissed
|
||||
// the FormState's dependenciesChanged() method runs. We're making sure
|
||||
// that the dependenciesChanged() method doesn't add an extra willPop
|
||||
// callback.
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pump(); // Start the pop "back" operation.
|
||||
await tester.pump(); // Call willPop which will show an Alert.
|
||||
await tester.tap(find.text('NO'));
|
||||
await tester.pump(); // Start the dismiss animation.
|
||||
await tester.pump(); // Resolve the willPop callback.
|
||||
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
|
||||
expect(find.text('Sample Form'), findsOneWidget);
|
||||
|
||||
// This time really dismiss the SampleForm by pressing the Alert's
|
||||
// YES button.
|
||||
await tester.tap(find.byTooltip('Back'));
|
||||
await tester.pump(); // Start the pop "back" operation.
|
||||
await tester.pump(); // Call willPop which will show an Alert.
|
||||
await tester.tap(find.text('YES'));
|
||||
await tester.pump(); // Start the dismiss animation.
|
||||
await tester.pump(); // Resolve the willPop callback.
|
||||
await tester.pump(const Duration(seconds: 1)); // Wait until it has finished.
|
||||
expect(find.text('Sample Form'), findsNothing);
|
||||
});
|
||||
|
||||
}
|
Loading…
Reference in a new issue