mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
Predictive back support for root routes (#120385)
This PR aims to support Android's predictive back gesture when popping the entire Flutter app. Predictive route transitions between routes inside of a Flutter app will come later. <img width="200" src="https://user-images.githubusercontent.com/389558/217918109-945febaa-9086-41cc-a476-1a189c7831d8.gif" /> ### Trying it out If you want to try this feature yourself, here are the necessary steps: 1. Run Android 33 or above. 1. Enable the feature flag for predictive back on the device under "Developer options". 1. Create a Flutter project, or clone [my example project](https://github.com/justinmc/flutter_predictive_back_examples). 1. Set `android:enableOnBackInvokedCallback="true"` in android/app/src/main/AndroidManifest.xml (already done in the example project). 1. Check out this branch. 1. Run the app. Perform a back gesture (swipe from the left side of the screen). You should see the predictive back animation like in the animation above and be able to commit or cancel it. ### go_router support go_router works with predictive back out of the box because it uses a Navigator internally that dispatches NavigationNotifications! ~~go_router can be supported by adding a listener to the router and updating SystemNavigator.setFrameworkHandlesBack.~~ Similar to with nested Navigators, nested go_routers is supported by using a PopScope widget. <details> <summary>Full example of nested go_routers</summary> ```dart // 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:go_router/go_router.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; void main() => runApp(_MyApp()); class _MyApp extends StatelessWidget { final GoRouter router = GoRouter( routes: <RouteBase>[ GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) => _HomePage(), ), GoRoute( path: '/nested_navigators', builder: (BuildContext context, GoRouterState state) => _NestedGoRoutersPage(), ), ], ); @override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: router, ); } } class _HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Nested Navigators Example'), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ const Text('Home Page'), const Text('A system back gesture here will exit the app.'), const SizedBox(height: 20.0), ListTile( title: const Text('Nested go_router route'), subtitle: const Text('This route has another go_router in addition to the one used with MaterialApp above.'), onTap: () { context.push('/nested_navigators'); }, ), ], ), ), ); } } class _NestedGoRoutersPage extends StatefulWidget { @override State<_NestedGoRoutersPage> createState() => _NestedGoRoutersPageState(); } class _NestedGoRoutersPageState extends State<_NestedGoRoutersPage> { late final GoRouter _router; final GlobalKey<NavigatorState> _nestedNavigatorKey = GlobalKey<NavigatorState>(); // If the nested navigator has routes that can be popped, then we want to // block the root navigator from handling the pop so that the nested navigator // can handle it instead. bool get _popEnabled { // canPop will throw an error if called before build. Is this the best way // to avoid that? return _nestedNavigatorKey.currentState == null ? true : !_router.canPop(); } void _onRouterChanged() { // Here the _router reports the location correctly, but canPop is still out // of date. Hence the post frame callback. SchedulerBinding.instance.addPostFrameCallback((Duration duration) { setState(() {}); }); } @override void initState() { super.initState(); final BuildContext rootContext = context; _router = GoRouter( navigatorKey: _nestedNavigatorKey, routes: [ GoRoute( path: '/', builder: (BuildContext context, GoRouterState state) => _LinksPage( title: 'Nested once - home route', backgroundColor: Colors.indigo, onBack: () { rootContext.pop(); }, buttons: <Widget>[ TextButton( onPressed: () { context.push('/two'); }, child: const Text('Go to another route in this nested Navigator'), ), ], ), ), GoRoute( path: '/two', builder: (BuildContext context, GoRouterState state) => _LinksPage( backgroundColor: Colors.indigo.withBlue(255), title: 'Nested once - page two', ), ), ], ); _router.addListener(_onRouterChanged); } @override void dispose() { _router.removeListener(_onRouterChanged); super.dispose(); } @override Widget build(BuildContext context) { return PopScope( popEnabled: _popEnabled, onPopped: (bool success) { if (success) { return; } _router.pop(); }, child: Router<Object>.withConfig( restorationScopeId: 'router-2', config: _router, ), ); } } class _LinksPage extends StatelessWidget { const _LinksPage ({ required this.backgroundColor, this.buttons = const <Widget>[], this.onBack, required this.title, }); final Color backgroundColor; final List<Widget> buttons; final VoidCallback? onBack; final String title; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: backgroundColor, body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Text(title), //const Text('A system back here will go back to Nested Navigators Page One'), ...buttons, TextButton( onPressed: onBack ?? () { context.pop(); }, child: const Text('Go back'), ), ], ), ), ); } } ``` </details> ### Resources Fixes https://github.com/flutter/flutter/issues/109513 Depends on engine PR https://github.com/flutter/engine/pull/39208 ✔️ Design doc: https://docs.google.com/document/d/1BGCWy1_LRrXEB6qeqTAKlk-U2CZlKJ5xI97g45U7azk/edit# Migration guide: https://github.com/flutter/website/pull/8952
This commit is contained in:
parent
daf5827975
commit
dedd100ebd
@ -48,9 +48,9 @@ class CupertinoNavigationDemo extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return PopScope(
|
||||||
// Prevent swipe popping of this page. Use explicit exit buttons only.
|
// Prevent swipe popping of this page. Use explicit exit buttons only.
|
||||||
onWillPop: () => Future<bool>.value(true),
|
canPop: false,
|
||||||
child: DefaultTextStyle(
|
child: DefaultTextStyle(
|
||||||
style: CupertinoTheme.of(context).textTheme.textStyle,
|
style: CupertinoTheme.of(context).textTheme.textStyle,
|
||||||
child: CupertinoTabScaffold(
|
child: CupertinoTabScaffold(
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
// This demo is based on
|
// This demo is based on
|
||||||
@ -109,16 +110,15 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
|
|||||||
bool _hasName = false;
|
bool _hasName = false;
|
||||||
late String _eventName;
|
late String _eventName;
|
||||||
|
|
||||||
Future<bool> _onWillPop() async {
|
Future<void> _handlePopInvoked(bool didPop) async {
|
||||||
_saveNeeded = _hasLocation || _hasName || _saveNeeded;
|
if (didPop) {
|
||||||
if (!_saveNeeded) {
|
return;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final ThemeData theme = Theme.of(context);
|
final ThemeData theme = Theme.of(context);
|
||||||
final TextStyle dialogTextStyle = theme.textTheme.titleMedium!.copyWith(color: theme.textTheme.bodySmall!.color);
|
final TextStyle dialogTextStyle = theme.textTheme.titleMedium!.copyWith(color: theme.textTheme.bodySmall!.color);
|
||||||
|
|
||||||
return showDialog<bool>(
|
final bool? shouldDiscard = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (BuildContext context) {
|
builder: (BuildContext context) {
|
||||||
return AlertDialog(
|
return AlertDialog(
|
||||||
@ -130,19 +130,31 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
|
|||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('CANCEL'),
|
child: const Text('CANCEL'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(false); // Pops the confirmation dialog but not the page.
|
// Pop the confirmation dialog and indicate that the page should
|
||||||
|
// not be popped.
|
||||||
|
Navigator.of(context).pop(false);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
TextButton(
|
TextButton(
|
||||||
child: const Text('DISCARD'),
|
child: const Text('DISCARD'),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pop(true); // Returning true to _onWillPop will pop again.
|
// Pop the confirmation dialog and indicate that the page should
|
||||||
|
// be popped, too.
|
||||||
|
Navigator.of(context).pop(true);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
) as Future<bool>;
|
);
|
||||||
|
|
||||||
|
if (shouldDiscard ?? false) {
|
||||||
|
// Since this is the root route, quit the app where possible by invoking
|
||||||
|
// the SystemNavigator. If this wasn't the root route, then
|
||||||
|
// Navigator.maybePop could be used instead.
|
||||||
|
// See https://github.com/flutter/flutter/issues/11490
|
||||||
|
SystemNavigator.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -162,7 +174,8 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: Form(
|
body: Form(
|
||||||
onWillPop: _onWillPop,
|
canPop: !_saveNeeded && !_hasLocation && !_hasName,
|
||||||
|
onPopInvoked: _handlePopInvoked,
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
primary: true,
|
primary: true,
|
||||||
|
@ -143,10 +143,9 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _warnUserAboutInvalidData() async {
|
Future<void> _handlePopInvoked(bool didPop) async {
|
||||||
final FormState? form = _formKey.currentState;
|
if (didPop) {
|
||||||
if (form == null || !_formWasEdited || form.validate()) {
|
return;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool? result = await showDialog<bool>(
|
final bool? result = await showDialog<bool>(
|
||||||
@ -168,7 +167,14 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return result!;
|
|
||||||
|
if (result ?? false) {
|
||||||
|
// Since this is the root route, quit the app where possible by invoking
|
||||||
|
// the SystemNavigator. If this wasn't the root route, then
|
||||||
|
// Navigator.maybePop could be used instead.
|
||||||
|
// See https://github.com/flutter/flutter/issues/11490
|
||||||
|
SystemNavigator.pop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -185,7 +191,8 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
|
|||||||
child: Form(
|
child: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
autovalidateMode: _autovalidateMode,
|
autovalidateMode: _autovalidateMode,
|
||||||
onWillPop: _warnUserAboutInvalidData,
|
canPop: _formKey.currentState == null || !_formWasEdited || _formKey.currentState!.validate(),
|
||||||
|
onPopInvoked: _handlePopInvoked,
|
||||||
child: Scrollbar(
|
child: Scrollbar(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
primary: true,
|
primary: true,
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:scoped_model/scoped_model.dart';
|
import 'package:scoped_model/scoped_model.dart';
|
||||||
|
|
||||||
import 'colors.dart';
|
import 'colors.dart';
|
||||||
@ -361,14 +360,12 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP
|
|||||||
|
|
||||||
// Closes the cart if the cart is open, otherwise exits the app (this should
|
// Closes the cart if the cart is open, otherwise exits the app (this should
|
||||||
// only be relevant for Android).
|
// only be relevant for Android).
|
||||||
Future<bool> _onWillPop() async {
|
void _handlePopInvoked(bool didPop) {
|
||||||
if (!_isOpen) {
|
if (didPop) {
|
||||||
await SystemNavigator.pop();
|
return;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
close();
|
close();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -378,8 +375,9 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP
|
|||||||
duration: const Duration(milliseconds: 225),
|
duration: const Duration(milliseconds: 225),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
alignment: FractionalOffset.topLeft,
|
alignment: FractionalOffset.topLeft,
|
||||||
child: WillPopScope(
|
child: PopScope(
|
||||||
onWillPop: _onWillPop,
|
canPop: !_isOpen,
|
||||||
|
onPopInvoked: _handlePopInvoked,
|
||||||
child: AnimatedBuilder(
|
child: AnimatedBuilder(
|
||||||
animation: widget.hideController,
|
animation: widget.hideController,
|
||||||
builder: _buildSlideAnimation,
|
builder: _buildSlideAnimation,
|
||||||
|
@ -325,14 +325,14 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
|
|||||||
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
|
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
bottom: false,
|
bottom: false,
|
||||||
child: WillPopScope(
|
child: PopScope(
|
||||||
onWillPop: () {
|
canPop: _category == null,
|
||||||
// Pop the category page if Android back button is pressed.
|
onPopInvoked: (bool didPop) {
|
||||||
if (_category != null) {
|
if (didPop) {
|
||||||
setState(() => _category = null);
|
return;
|
||||||
return Future<bool>.value(false);
|
|
||||||
}
|
}
|
||||||
return Future<bool>.value(true);
|
// Pop the category page if Android back button is pressed.
|
||||||
|
setState(() => _category = null);
|
||||||
},
|
},
|
||||||
child: Backdrop(
|
child: Backdrop(
|
||||||
backTitle: const Text('Options'),
|
backTitle: const Text('Options'),
|
||||||
|
@ -71,14 +71,10 @@ class _HomeState extends State<Home> with TickerProviderStateMixin<Home> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return WillPopScope(
|
return NavigatorPopHandler(
|
||||||
onWillPop: () async {
|
onPop: () {
|
||||||
final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!;
|
final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!;
|
||||||
if (!navigator.canPop()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
navigator.pop();
|
navigator.pop();
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
166
examples/api/lib/widgets/form/form.1.dart
Normal file
166
examples/api/lib/widgets/form/form.1.dart
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
// 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/services.dart';
|
||||||
|
|
||||||
|
/// This sample demonstrates showing a confirmation dialog when the user
|
||||||
|
/// attempts to navigate away from a page with unsaved [Form] data.
|
||||||
|
|
||||||
|
void main() => runApp(const FormApp());
|
||||||
|
|
||||||
|
class FormApp extends StatelessWidget {
|
||||||
|
const FormApp({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Confirmation Dialog Example'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: _SaveableForm(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaveableForm extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<_SaveableForm> createState() => _SaveableFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SaveableFormState extends State<_SaveableForm> {
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
String _savedValue = '';
|
||||||
|
bool _isDirty = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller.addListener(_onChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.removeListener(_onChanged);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChanged() {
|
||||||
|
final bool nextIsDirty = _savedValue != _controller.text;
|
||||||
|
if (nextIsDirty == _isDirty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_isDirty = nextIsDirty;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _showDialog() async {
|
||||||
|
final bool? shouldDiscard = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Are you sure?'),
|
||||||
|
content: const Text('Any unsaved changes will be lost!'),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Yes, discard my changes'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
child: const Text('No, continue editing'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (shouldDiscard ?? false) {
|
||||||
|
// Since this is the root route, quit the app where possible by invoking
|
||||||
|
// the SystemNavigator. If this wasn't the root route, then
|
||||||
|
// Navigator.maybePop could be used instead.
|
||||||
|
// See https://github.com/flutter/flutter/issues/11490
|
||||||
|
SystemNavigator.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _save(String? value) {
|
||||||
|
setState(() {
|
||||||
|
_savedValue = value ?? '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('If the field below is unsaved, a confirmation dialog will be shown on back.'),
|
||||||
|
const SizedBox(height: 20.0),
|
||||||
|
Form(
|
||||||
|
canPop: !_isDirty,
|
||||||
|
onPopInvoked: (bool didPop) {
|
||||||
|
if (didPop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_showDialog();
|
||||||
|
},
|
||||||
|
autovalidateMode: AutovalidateMode.always,
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
TextFormField(
|
||||||
|
controller: _controller,
|
||||||
|
onFieldSubmitted: (String? value) {
|
||||||
|
_save(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_save(_controller.text);
|
||||||
|
},
|
||||||
|
child: Row(
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Save'),
|
||||||
|
if (_controller.text.isNotEmpty)
|
||||||
|
Icon(
|
||||||
|
_isDirty ? Icons.warning : Icons.check,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_isDirty) {
|
||||||
|
_showDialog();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Since this is the root route, quit the app where possible by
|
||||||
|
// invoking the SystemNavigator. If this wasn't the root route,
|
||||||
|
// then Navigator.maybePop could be used instead.
|
||||||
|
// See https://github.com/flutter/flutter/issues/11490
|
||||||
|
SystemNavigator.pop();
|
||||||
|
},
|
||||||
|
child: const Text('Go back'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,164 @@
|
|||||||
|
// 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';
|
||||||
|
|
||||||
|
/// This sample demonstrates using [NavigatorPopHandler] to handle system back
|
||||||
|
/// gestures when there are nested [Navigator] widgets by delegating to the
|
||||||
|
/// current [Navigator].
|
||||||
|
|
||||||
|
void main() => runApp(const NavigatorPopHandlerApp());
|
||||||
|
|
||||||
|
class NavigatorPopHandlerApp extends StatelessWidget {
|
||||||
|
const NavigatorPopHandlerApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => _HomePage(),
|
||||||
|
'/nested_navigators': (BuildContext context) => const NestedNavigatorsPage(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePage extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Nested Navigators Example'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Home Page'),
|
||||||
|
const Text('A system back gesture here will exit the app.'),
|
||||||
|
const SizedBox(height: 20.0),
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Nested Navigator route'),
|
||||||
|
subtitle: const Text('This route has another Navigator widget in addition to the one inside MaterialApp above.'),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.of(context).pushNamed('/nested_navigators');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NestedNavigatorsPage extends StatefulWidget {
|
||||||
|
const NestedNavigatorsPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NestedNavigatorsPage> createState() => _NestedNavigatorsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NestedNavigatorsPageState extends State<NestedNavigatorsPage> {
|
||||||
|
final GlobalKey<NavigatorState> _nestedNavigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return NavigatorPopHandler(
|
||||||
|
onPop: () {
|
||||||
|
_nestedNavigatorKey.currentState!.maybePop();
|
||||||
|
},
|
||||||
|
child: Navigator(
|
||||||
|
key: _nestedNavigatorKey,
|
||||||
|
initialRoute: 'nested_navigators/one',
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
switch (settings.name) {
|
||||||
|
case 'nested_navigators/one':
|
||||||
|
final BuildContext rootContext = context;
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (BuildContext context) => NestedNavigatorsPageOne(
|
||||||
|
onBack: () {
|
||||||
|
Navigator.of(rootContext).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case 'nested_navigators/one/another_one':
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (BuildContext context) => const NestedNavigatorsPageTwo(
|
||||||
|
),
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw Exception('Invalid route: ${settings.name}');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NestedNavigatorsPageOne extends StatelessWidget {
|
||||||
|
const NestedNavigatorsPageOne({
|
||||||
|
required this.onBack,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final VoidCallback onBack;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey,
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Nested Navigators Page One'),
|
||||||
|
const Text('A system back here returns to the home page.'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('nested_navigators/one/another_one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to another route in this nested Navigator'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
// Can't use Navigator.of(context).pop() because this is the root
|
||||||
|
// route, so it can't be popped. The Navigator above this needs to
|
||||||
|
// be popped.
|
||||||
|
onPressed: onBack,
|
||||||
|
child: const Text('Go back'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NestedNavigatorsPageTwo extends StatelessWidget {
|
||||||
|
const NestedNavigatorsPageTwo({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.grey.withBlue(180),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Nested Navigators Page Two'),
|
||||||
|
const Text('A system back here will go back to Nested Navigators Page One'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Go back'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,250 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This sample demonstrates nested navigation in a bottom navigation bar.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// There are three possible tabs.
|
||||||
|
enum _Tab {
|
||||||
|
home,
|
||||||
|
one,
|
||||||
|
two,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each tab has two possible pages.
|
||||||
|
enum _TabPage {
|
||||||
|
home,
|
||||||
|
one,
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef _TabPageCallback = void Function(List<_TabPage> pages);
|
||||||
|
|
||||||
|
void main() => runApp(const NavigatorPopHandlerApp());
|
||||||
|
|
||||||
|
class NavigatorPopHandlerApp extends StatelessWidget {
|
||||||
|
const NavigatorPopHandlerApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
initialRoute: '/home',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/home': (BuildContext context) => const _BottomNavPage(
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomNavPage extends StatefulWidget {
|
||||||
|
const _BottomNavPage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BottomNavPage> createState() => _BottomNavPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomNavPageState extends State<_BottomNavPage> {
|
||||||
|
_Tab _tab = _Tab.home;
|
||||||
|
|
||||||
|
final GlobalKey _tabHomeKey = GlobalKey();
|
||||||
|
final GlobalKey _tabOneKey = GlobalKey();
|
||||||
|
final GlobalKey _tabTwoKey = GlobalKey();
|
||||||
|
|
||||||
|
List<_TabPage> _tabHomePages = <_TabPage>[_TabPage.home];
|
||||||
|
List<_TabPage> _tabOnePages = <_TabPage>[_TabPage.home];
|
||||||
|
List<_TabPage> _tabTwoPages = <_TabPage>[_TabPage.home];
|
||||||
|
|
||||||
|
BottomNavigationBarItem _itemForPage(_Tab page) {
|
||||||
|
switch (page) {
|
||||||
|
case _Tab.home:
|
||||||
|
return const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.home),
|
||||||
|
label: 'Go to Home',
|
||||||
|
);
|
||||||
|
case _Tab.one:
|
||||||
|
return const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.one_k),
|
||||||
|
label: 'Go to One',
|
||||||
|
);
|
||||||
|
case _Tab.two:
|
||||||
|
return const BottomNavigationBarItem(
|
||||||
|
icon: Icon(Icons.two_k),
|
||||||
|
label: 'Go to Two',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _getPage(_Tab page) {
|
||||||
|
switch (page) {
|
||||||
|
case _Tab.home:
|
||||||
|
return _BottomNavTab(
|
||||||
|
key: _tabHomeKey,
|
||||||
|
title: 'Home Tab',
|
||||||
|
color: Colors.grey,
|
||||||
|
pages: _tabHomePages,
|
||||||
|
onChangedPages: (List<_TabPage> pages) {
|
||||||
|
setState(() {
|
||||||
|
_tabHomePages = pages;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case _Tab.one:
|
||||||
|
return _BottomNavTab(
|
||||||
|
key: _tabOneKey,
|
||||||
|
title: 'Tab One',
|
||||||
|
color: Colors.amber,
|
||||||
|
pages: _tabOnePages,
|
||||||
|
onChangedPages: (List<_TabPage> pages) {
|
||||||
|
setState(() {
|
||||||
|
_tabOnePages = pages;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case _Tab.two:
|
||||||
|
return _BottomNavTab(
|
||||||
|
key: _tabTwoKey,
|
||||||
|
title: 'Tab Two',
|
||||||
|
color: Colors.blueGrey,
|
||||||
|
pages: _tabTwoPages,
|
||||||
|
onChangedPages: (List<_TabPage> pages) {
|
||||||
|
setState(() {
|
||||||
|
_tabTwoPages = pages;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onItemTapped(int index) {
|
||||||
|
setState(() {
|
||||||
|
_tab = _Tab.values.elementAt(index);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: _getPage(_tab),
|
||||||
|
),
|
||||||
|
bottomNavigationBar: BottomNavigationBar(
|
||||||
|
items: _Tab.values.map(_itemForPage).toList(),
|
||||||
|
currentIndex: _Tab.values.indexOf(_tab),
|
||||||
|
selectedItemColor: Colors.amber[800],
|
||||||
|
onTap: _onItemTapped,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomNavTab extends StatefulWidget {
|
||||||
|
const _BottomNavTab({
|
||||||
|
super.key,
|
||||||
|
required this.color,
|
||||||
|
required this.onChangedPages,
|
||||||
|
required this.pages,
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Color color;
|
||||||
|
final _TabPageCallback onChangedPages;
|
||||||
|
final List<_TabPage> pages;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BottomNavTab> createState() => _BottomNavTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BottomNavTabState extends State<_BottomNavTab> {
|
||||||
|
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return NavigatorPopHandler(
|
||||||
|
onPop: () {
|
||||||
|
_navigatorKey.currentState?.maybePop();
|
||||||
|
},
|
||||||
|
child: Navigator(
|
||||||
|
key: _navigatorKey,
|
||||||
|
onPopPage: (Route<void> route, void result) {
|
||||||
|
if (!route.didPop(null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
widget.onChangedPages(<_TabPage>[
|
||||||
|
...widget.pages,
|
||||||
|
]..removeLast());
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
pages: widget.pages.map((_TabPage page) {
|
||||||
|
switch (page) {
|
||||||
|
case _TabPage.home:
|
||||||
|
return MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
title: 'Bottom nav - tab ${widget.title} - route $page',
|
||||||
|
backgroundColor: widget.color,
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
widget.onChangedPages(<_TabPage>[
|
||||||
|
...widget.pages,
|
||||||
|
_TabPage.one,
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
child: const Text('Go to another route in this nested Navigator'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case _TabPage.one:
|
||||||
|
return MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
backgroundColor: widget.color,
|
||||||
|
title: 'Bottom nav - tab ${widget.title} - route $page',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
widget.onChangedPages(<_TabPage>[
|
||||||
|
...widget.pages,
|
||||||
|
]..removeLast());
|
||||||
|
},
|
||||||
|
child: const Text('Go back'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinksPage extends StatelessWidget {
|
||||||
|
const _LinksPage ({
|
||||||
|
required this.backgroundColor,
|
||||||
|
this.buttons = const <Widget>[],
|
||||||
|
required this.title,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Color backgroundColor;
|
||||||
|
final List<Widget> buttons;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title),
|
||||||
|
...buttons,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
128
examples/api/lib/widgets/pop_scope/pop_scope.0.dart
Normal file
128
examples/api/lib/widgets/pop_scope/pop_scope.0.dart
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// This sample demonstrates showing a confirmation dialog before navigating
|
||||||
|
// away from a page.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const NavigatorPopHandlerApp());
|
||||||
|
|
||||||
|
class NavigatorPopHandlerApp extends StatelessWidget {
|
||||||
|
const NavigatorPopHandlerApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
initialRoute: '/home',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/home': (BuildContext context) => const _HomePage(),
|
||||||
|
'/two': (BuildContext context) => const _PageTwo(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePage extends StatefulWidget {
|
||||||
|
const _HomePage();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<_HomePage> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Page One'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/two');
|
||||||
|
},
|
||||||
|
child: const Text('Next page'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PageTwo extends StatefulWidget {
|
||||||
|
const _PageTwo();
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_PageTwo> createState() => _PageTwoState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PageTwoState extends State<_PageTwo> {
|
||||||
|
void _showBackDialog() {
|
||||||
|
showDialog<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Are you sure?'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to leave this page?',
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
child: const Text('Nevermind'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||||
|
),
|
||||||
|
child: const Text('Leave'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Page Two'),
|
||||||
|
PopScope(
|
||||||
|
canPop: false,
|
||||||
|
onPopInvoked: (bool didPop) {
|
||||||
|
if (didPop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_showBackDialog();
|
||||||
|
},
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
_showBackDialog();
|
||||||
|
},
|
||||||
|
child: const Text('Go back'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,77 +0,0 @@
|
|||||||
// 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';
|
|
||||||
|
|
||||||
/// Flutter code sample for [WillPopScope].
|
|
||||||
|
|
||||||
void main() => runApp(const WillPopScopeExampleApp());
|
|
||||||
|
|
||||||
class WillPopScopeExampleApp extends StatelessWidget {
|
|
||||||
const WillPopScopeExampleApp({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return const MaterialApp(
|
|
||||||
home: WillPopScopeExample(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WillPopScopeExample extends StatefulWidget {
|
|
||||||
const WillPopScopeExample({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<WillPopScopeExample> createState() => _WillPopScopeExampleState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WillPopScopeExampleState extends State<WillPopScopeExample> {
|
|
||||||
bool shouldPop = true;
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return WillPopScope(
|
|
||||||
onWillPop: () async {
|
|
||||||
return shouldPop;
|
|
||||||
},
|
|
||||||
child: Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: const Text('Flutter WillPopScope demo'),
|
|
||||||
),
|
|
||||||
body: Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: <Widget>[
|
|
||||||
OutlinedButton(
|
|
||||||
child: const Text('Push'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).push<void>(
|
|
||||||
MaterialPageRoute<void>(
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return const WillPopScopeExample();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
OutlinedButton(
|
|
||||||
child: Text('shouldPop: $shouldPop'),
|
|
||||||
onPressed: () {
|
|
||||||
setState(
|
|
||||||
() {
|
|
||||||
shouldPop = !shouldPop;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const Text('Push to a new screen, then tap on shouldPop '
|
|
||||||
'button to toggle its value. Press the back '
|
|
||||||
'button in the appBar to check its behavior '
|
|
||||||
'for different values of shouldPop'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
37
examples/api/test/widgets/form/form.1_test.dart
Normal file
37
examples/api/test/widgets/form/form.1_test.dart
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// 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_api_samples/widgets/form/form.1.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Can go back when form is clean', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.FormApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go back'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Cannot go back when form is dirty', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.FormApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextFormField), 'some new text');
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go back'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Are you sure?'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
// 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/navigator_pop_handler/navigator_pop_handler.0.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../navigator_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Can go back with system back gesture', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.NavigatorPopHandlerApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Nested Navigators Example'), findsOneWidget);
|
||||||
|
expect(find.text('Nested Navigators Page One'), findsNothing);
|
||||||
|
expect(find.text('Nested Navigators Page Two'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Nested Navigator route'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested Navigators Example'), findsNothing);
|
||||||
|
expect(find.text('Nested Navigators Page One'), findsOneWidget);
|
||||||
|
expect(find.text('Nested Navigators Page Two'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to another route in this nested Navigator'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested Navigators Example'), findsNothing);
|
||||||
|
expect(find.text('Nested Navigators Page One'), findsNothing);
|
||||||
|
expect(find.text('Nested Navigators Page Two'), findsOneWidget);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested Navigators Example'), findsNothing);
|
||||||
|
expect(find.text('Nested Navigators Page One'), findsOneWidget);
|
||||||
|
expect(find.text('Nested Navigators Page Two'), findsNothing);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested Navigators Example'), findsOneWidget);
|
||||||
|
expect(find.text('Nested Navigators Page One'), findsNothing);
|
||||||
|
expect(find.text('Nested Navigators Page Two'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
// 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/navigator_pop_handler/navigator_pop_handler.1.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../navigator_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets("System back gesture operates on current tab's nested Navigator", (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.NavigatorPopHandlerApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget);
|
||||||
|
|
||||||
|
// Go to the next route in this tab.
|
||||||
|
await tester.tap(find.text('Go to another route in this nested Navigator'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget);
|
||||||
|
|
||||||
|
// Go to another tab.
|
||||||
|
await tester.tap(find.text('Go to One'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Bottom nav - tab Tab One - route _TabPage.home'), findsOneWidget);
|
||||||
|
|
||||||
|
// Return to the home tab. The navigation state is preserved.
|
||||||
|
await tester.tap(find.text('Go to Home'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.one'), findsOneWidget);
|
||||||
|
|
||||||
|
// A back pops the navigation stack of the current tab's nested Navigator.
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Bottom nav - tab Home Tab - route _TabPage.home'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
20
examples/api/test/widgets/navigator_utils.dart
Normal file
20
examples/api/test/widgets/navigator_utils.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 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/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Simulates a system back, like a back gesture on Android.
|
||||||
|
///
|
||||||
|
/// Sends the same platform channel message that the engine sends when it
|
||||||
|
/// receives a system back.
|
||||||
|
Future<void> simulateSystemBack() {
|
||||||
|
return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
||||||
|
'flutter/navigation',
|
||||||
|
const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
||||||
|
'method': 'popRoute',
|
||||||
|
}),
|
||||||
|
(ByteData? _) {},
|
||||||
|
);
|
||||||
|
}
|
66
examples/api/test/widgets/pop_scope/pop_scope.0_test.dart
Normal file
66
examples/api/test/widgets/pop_scope/pop_scope.0_test.dart
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
// 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/pop_scope/pop_scope.0.dart' as example;
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import '../navigator_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Can choose to stay on page', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.NavigatorPopHandlerApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Page One'), findsOneWidget);
|
||||||
|
expect(find.text('Page Two'), findsNothing);
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next page'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page One'), findsNothing);
|
||||||
|
expect(find.text('Page Two'), findsOneWidget);
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page One'), findsNothing);
|
||||||
|
expect(find.text('Page Two'), findsOneWidget);
|
||||||
|
expect(find.text('Are you sure?'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Nevermind'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page One'), findsNothing);
|
||||||
|
expect(find.text('Page Two'), findsOneWidget);
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('Can choose to go back', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const example.NavigatorPopHandlerApp(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Page One'), findsOneWidget);
|
||||||
|
expect(find.text('Page Two'), findsNothing);
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next page'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page One'), findsNothing);
|
||||||
|
expect(find.text('Page Two'), findsOneWidget);
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page One'), findsNothing);
|
||||||
|
expect(find.text('Page Two'), findsOneWidget);
|
||||||
|
expect(find.text('Are you sure?'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Leave'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page One'), findsOneWidget);
|
||||||
|
expect(find.text('Page Two'), findsNothing);
|
||||||
|
expect(find.text('Are you sure?'), findsNothing);
|
||||||
|
});
|
||||||
|
}
|
@ -1,32 +0,0 @@
|
|||||||
// 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/will_pop_scope/will_pop_scope.0.dart' as example;
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
testWidgets('pressing shouldPop button changes shouldPop', (WidgetTester tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
const example.WillPopScopeExampleApp(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final Finder buttonFinder = find.text('shouldPop: true');
|
|
||||||
expect(buttonFinder, findsOneWidget);
|
|
||||||
await tester.tap(buttonFinder);
|
|
||||||
await tester.pump();
|
|
||||||
expect(find.text('shouldPop: false'), findsOneWidget);
|
|
||||||
});
|
|
||||||
testWidgets('pressing Push button pushes route', (WidgetTester tester) async {
|
|
||||||
await tester.pumpWidget(
|
|
||||||
const example.WillPopScopeExampleApp(),
|
|
||||||
);
|
|
||||||
|
|
||||||
final Finder buttonFinder = find.text('Push');
|
|
||||||
expect(buttonFinder, findsOneWidget);
|
|
||||||
expect(find.byType(example.WillPopScopeExample), findsOneWidget);
|
|
||||||
await tester.tap(buttonFinder);
|
|
||||||
await tester.pumpAndSettle();
|
|
||||||
expect(find.byType(example.WillPopScopeExample, skipOffstage: false), findsNWidgets(2));
|
|
||||||
});
|
|
||||||
}
|
|
@ -157,6 +157,7 @@ class CupertinoApp extends StatefulWidget {
|
|||||||
this.onGenerateRoute,
|
this.onGenerateRoute,
|
||||||
this.onGenerateInitialRoutes,
|
this.onGenerateInitialRoutes,
|
||||||
this.onUnknownRoute,
|
this.onUnknownRoute,
|
||||||
|
this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
|
||||||
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
|
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
|
||||||
this.builder,
|
this.builder,
|
||||||
this.title = '',
|
this.title = '',
|
||||||
@ -202,6 +203,7 @@ class CupertinoApp extends StatefulWidget {
|
|||||||
this.builder,
|
this.builder,
|
||||||
this.title = '',
|
this.title = '',
|
||||||
this.onGenerateTitle,
|
this.onGenerateTitle,
|
||||||
|
this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
|
||||||
this.color,
|
this.color,
|
||||||
this.locale,
|
this.locale,
|
||||||
this.localizationsDelegates,
|
this.localizationsDelegates,
|
||||||
@ -268,6 +270,9 @@ class CupertinoApp extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
|
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
|
||||||
final RouteFactory? onUnknownRoute;
|
final RouteFactory? onUnknownRoute;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.widgetsApp.onNavigationNotification}
|
||||||
|
final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
|
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
|
||||||
final List<NavigatorObserver>? navigatorObservers;
|
final List<NavigatorObserver>? navigatorObservers;
|
||||||
|
|
||||||
@ -573,6 +578,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
|
|||||||
onGenerateRoute: widget.onGenerateRoute,
|
onGenerateRoute: widget.onGenerateRoute,
|
||||||
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
||||||
onUnknownRoute: widget.onUnknownRoute,
|
onUnknownRoute: widget.onUnknownRoute,
|
||||||
|
onNavigationNotification: widget.onNavigationNotification,
|
||||||
builder: widget.builder,
|
builder: widget.builder,
|
||||||
title: widget.title,
|
title: widget.title,
|
||||||
onGenerateTitle: widget.onGenerateTitle,
|
onGenerateTitle: widget.onGenerateTitle,
|
||||||
|
@ -196,7 +196,8 @@ mixin CupertinoRouteTransitionMixin<T> on PageRoute<T> {
|
|||||||
}
|
}
|
||||||
// If attempts to dismiss this route might be vetoed such as in a page
|
// If attempts to dismiss this route might be vetoed such as in a page
|
||||||
// with forms, then do not allow the user to dismiss the route with a swipe.
|
// with forms, then do not allow the user to dismiss the route with a swipe.
|
||||||
if (route.hasScopedWillPopCallback) {
|
if (route.hasScopedWillPopCallback
|
||||||
|
|| route.popDisposition == RoutePopDisposition.doNotPop) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Fullscreen dialogs aren't dismissible by back swipe.
|
// Fullscreen dialogs aren't dismissible by back swipe.
|
||||||
|
@ -162,15 +162,39 @@ class _CupertinoTabViewState extends State<CupertinoTabView> {
|
|||||||
..add(_heroController);
|
..add(_heroController);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalKey<NavigatorState>? _ownedNavigatorKey;
|
||||||
|
GlobalKey<NavigatorState> get _navigatorKey {
|
||||||
|
if (widget.navigatorKey != null) {
|
||||||
|
return widget.navigatorKey!;
|
||||||
|
}
|
||||||
|
_ownedNavigatorKey ??= GlobalKey<NavigatorState>();
|
||||||
|
return _ownedNavigatorKey!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether this tab is currently the active tab.
|
||||||
|
bool get _isActive => TickerMode.of(context);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Navigator(
|
final Widget child = Navigator(
|
||||||
key: widget.navigatorKey,
|
key: _navigatorKey,
|
||||||
onGenerateRoute: _onGenerateRoute,
|
onGenerateRoute: _onGenerateRoute,
|
||||||
onUnknownRoute: _onUnknownRoute,
|
onUnknownRoute: _onUnknownRoute,
|
||||||
observers: _navigatorObservers,
|
observers: _navigatorObservers,
|
||||||
restorationScopeId: widget.restorationScopeId,
|
restorationScopeId: widget.restorationScopeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle system back gestures only if the tab is currently active.
|
||||||
|
return NavigatorPopHandler(
|
||||||
|
enabled: _isActive,
|
||||||
|
onPop: () {
|
||||||
|
if (!_isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_navigatorKey.currentState!.pop();
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
|
||||||
|
@ -1179,9 +1179,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp
|
|||||||
_builtLayout = _LayoutMode.nested;
|
_builtLayout = _LayoutMode.nested;
|
||||||
final MaterialPageRoute<void> masterPageRoute = _masterPageRoute(context);
|
final MaterialPageRoute<void> masterPageRoute = _masterPageRoute(context);
|
||||||
|
|
||||||
return WillPopScope(
|
return NavigatorPopHandler(
|
||||||
// Push pop check into nested navigator.
|
onPop: () {
|
||||||
onWillPop: () async => !(await _navigatorKey.currentState!.maybePop()),
|
_navigatorKey.currentState!.maybePop();
|
||||||
|
},
|
||||||
child: Navigator(
|
child: Navigator(
|
||||||
key: _navigatorKey,
|
key: _navigatorKey,
|
||||||
initialRoute: 'initial',
|
initialRoute: 'initial',
|
||||||
@ -1234,12 +1235,10 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp
|
|||||||
|
|
||||||
MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
|
MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
|
||||||
return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
|
return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
|
||||||
return WillPopScope(
|
return PopScope(
|
||||||
onWillPop: () async {
|
onPopInvoked: (bool didPop) {
|
||||||
// No need for setState() as rebuild happens on navigation pop.
|
// No need for setState() as rebuild happens on navigation pop.
|
||||||
focus = _Focus.master;
|
focus = _Focus.master;
|
||||||
Navigator.of(context).pop();
|
|
||||||
return false;
|
|
||||||
},
|
},
|
||||||
child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)),
|
child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)),
|
||||||
);
|
);
|
||||||
|
@ -214,6 +214,7 @@ class MaterialApp extends StatefulWidget {
|
|||||||
this.onGenerateRoute,
|
this.onGenerateRoute,
|
||||||
this.onGenerateInitialRoutes,
|
this.onGenerateInitialRoutes,
|
||||||
this.onUnknownRoute,
|
this.onUnknownRoute,
|
||||||
|
this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
|
||||||
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
|
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
|
||||||
this.builder,
|
this.builder,
|
||||||
this.title = '',
|
this.title = '',
|
||||||
@ -267,6 +268,7 @@ class MaterialApp extends StatefulWidget {
|
|||||||
this.builder,
|
this.builder,
|
||||||
this.title = '',
|
this.title = '',
|
||||||
this.onGenerateTitle,
|
this.onGenerateTitle,
|
||||||
|
this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
|
||||||
this.color,
|
this.color,
|
||||||
this.theme,
|
this.theme,
|
||||||
this.darkTheme,
|
this.darkTheme,
|
||||||
@ -343,6 +345,9 @@ class MaterialApp extends StatefulWidget {
|
|||||||
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
|
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
|
||||||
final RouteFactory? onUnknownRoute;
|
final RouteFactory? onUnknownRoute;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.widgetsApp.onNavigationNotification}
|
||||||
|
final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
|
||||||
|
|
||||||
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
|
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
|
||||||
final List<NavigatorObserver>? navigatorObservers;
|
final List<NavigatorObserver>? navigatorObservers;
|
||||||
|
|
||||||
@ -1019,6 +1024,7 @@ class _MaterialAppState extends State<MaterialApp> {
|
|||||||
onGenerateRoute: widget.onGenerateRoute,
|
onGenerateRoute: widget.onGenerateRoute,
|
||||||
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
|
||||||
onUnknownRoute: widget.onUnknownRoute,
|
onUnknownRoute: widget.onUnknownRoute,
|
||||||
|
onNavigationNotification: widget.onNavigationNotification,
|
||||||
builder: _materialBuilder,
|
builder: _materialBuilder,
|
||||||
title: widget.title,
|
title: widget.title,
|
||||||
onGenerateTitle: widget.onGenerateTitle,
|
onGenerateTitle: widget.onGenerateTitle,
|
||||||
|
@ -2,10 +2,44 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import 'system_channels.dart';
|
import 'system_channels.dart';
|
||||||
|
|
||||||
/// Controls specific aspects of the system navigation stack.
|
/// Controls specific aspects of the system navigation stack.
|
||||||
abstract final class SystemNavigator {
|
abstract final class SystemNavigator {
|
||||||
|
/// Informs the platform of whether or not the Flutter framework will handle
|
||||||
|
/// back events.
|
||||||
|
///
|
||||||
|
/// Currently, this is used only on Android to inform its use of the
|
||||||
|
/// predictive back gesture when exiting the app. When true, predictive back
|
||||||
|
/// is disabled.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * The
|
||||||
|
/// [migration guide](https://developer.android.com/guide/navigation/predictive-back-gesture)
|
||||||
|
/// for predictive back in native Android apps.
|
||||||
|
static Future<void> setFrameworkHandlesBack(bool frameworkHandlesBack) async {
|
||||||
|
// Currently, this method call is only relevant on Android.
|
||||||
|
if (kIsWeb) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return;
|
||||||
|
case TargetPlatform.android:
|
||||||
|
return SystemChannels.platform.invokeMethod<void>(
|
||||||
|
'SystemNavigator.setFrameworkHandlesBack',
|
||||||
|
frameworkHandlesBack,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Removes the topmost Flutter instance, presenting what was before
|
/// Removes the topmost Flutter instance, presenting what was before
|
||||||
/// it.
|
/// it.
|
||||||
///
|
///
|
||||||
|
@ -19,6 +19,7 @@ import 'framework.dart';
|
|||||||
import 'localizations.dart';
|
import 'localizations.dart';
|
||||||
import 'media_query.dart';
|
import 'media_query.dart';
|
||||||
import 'navigator.dart';
|
import 'navigator.dart';
|
||||||
|
import 'notification_listener.dart';
|
||||||
import 'pages.dart';
|
import 'pages.dart';
|
||||||
import 'performance_overlay.dart';
|
import 'performance_overlay.dart';
|
||||||
import 'restoration.dart';
|
import 'restoration.dart';
|
||||||
@ -313,6 +314,7 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
this.onGenerateRoute,
|
this.onGenerateRoute,
|
||||||
this.onGenerateInitialRoutes,
|
this.onGenerateInitialRoutes,
|
||||||
this.onUnknownRoute,
|
this.onUnknownRoute,
|
||||||
|
this.onNavigationNotification = defaultOnNavigationNotification,
|
||||||
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
|
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
|
||||||
this.initialRoute,
|
this.initialRoute,
|
||||||
this.pageRouteBuilder,
|
this.pageRouteBuilder,
|
||||||
@ -420,6 +422,7 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
this.builder,
|
this.builder,
|
||||||
this.title = '',
|
this.title = '',
|
||||||
this.onGenerateTitle,
|
this.onGenerateTitle,
|
||||||
|
this.onNavigationNotification = defaultOnNavigationNotification,
|
||||||
this.textStyle,
|
this.textStyle,
|
||||||
required this.color,
|
required this.color,
|
||||||
this.locale,
|
this.locale,
|
||||||
@ -701,6 +704,17 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
final RouteFactory? onUnknownRoute;
|
final RouteFactory? onUnknownRoute;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.widgetsApp.onNavigationNotification}
|
||||||
|
/// The callback to use when receiving a [NavigationNotification].
|
||||||
|
///
|
||||||
|
/// By default set to [WidgetsApp.defaultOnNavigationNotification], which
|
||||||
|
/// updates the engine with the navigation status.
|
||||||
|
///
|
||||||
|
/// If null, [NavigationNotification] is not listened for at all, and so will
|
||||||
|
/// continue to propagate.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
|
||||||
|
|
||||||
/// {@template flutter.widgets.widgetsApp.initialRoute}
|
/// {@template flutter.widgets.widgetsApp.initialRoute}
|
||||||
/// The name of the first route to show, if a [Navigator] is built.
|
/// The name of the first route to show, if a [Navigator] is built.
|
||||||
///
|
///
|
||||||
@ -1314,6 +1328,15 @@ class WidgetsApp extends StatefulWidget {
|
|||||||
VoidCallbackIntent: VoidCallbackAction(),
|
VoidCallbackIntent: VoidCallbackAction(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// The default value for [onNavigationNotification].
|
||||||
|
///
|
||||||
|
/// Updates the platform with [NavigationNotification.canHandlePop] and stops
|
||||||
|
/// bubbling.
|
||||||
|
static bool defaultOnNavigationNotification(NavigationNotification notification) {
|
||||||
|
SystemNavigator.setFrameworkHandlesBack(notification.canHandlePop);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<WidgetsApp> createState() => _WidgetsAppState();
|
State<WidgetsApp> createState() => _WidgetsAppState();
|
||||||
}
|
}
|
||||||
@ -1748,30 +1771,25 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
assert(_debugCheckLocalizations(appLocale));
|
assert(_debugCheckLocalizations(appLocale));
|
||||||
|
|
||||||
return RootRestorationScope(
|
Widget child = Shortcuts(
|
||||||
restorationId: widget.restorationScopeId,
|
debugLabel: '<Default WidgetsApp Shortcuts>',
|
||||||
child: SharedAppData(
|
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
|
||||||
child: Shortcuts(
|
// DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
|
||||||
debugLabel: '<Default WidgetsApp Shortcuts>',
|
// fall through to the defaultShortcuts.
|
||||||
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
|
child: DefaultTextEditingShortcuts(
|
||||||
// DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
|
child: Actions(
|
||||||
// fall through to the defaultShortcuts.
|
actions: widget.actions ?? <Type, Action<Intent>>{
|
||||||
child: DefaultTextEditingShortcuts(
|
...WidgetsApp.defaultActions,
|
||||||
child: Actions(
|
ScrollIntent: Action<ScrollIntent>.overridable(context: context, defaultAction: ScrollAction()),
|
||||||
actions: widget.actions ?? <Type, Action<Intent>>{
|
},
|
||||||
...WidgetsApp.defaultActions,
|
child: FocusTraversalGroup(
|
||||||
ScrollIntent: Action<ScrollIntent>.overridable(context: context, defaultAction: ScrollAction()),
|
policy: ReadingOrderTraversalPolicy(),
|
||||||
},
|
child: TapRegionSurface(
|
||||||
child: FocusTraversalGroup(
|
child: ShortcutRegistrar(
|
||||||
policy: ReadingOrderTraversalPolicy(),
|
child: Localizations(
|
||||||
child: TapRegionSurface(
|
locale: appLocale,
|
||||||
child: ShortcutRegistrar(
|
delegates: _localizationsDelegates.toList(),
|
||||||
child: Localizations(
|
child: title,
|
||||||
locale: appLocale,
|
|
||||||
delegates: _localizationsDelegates.toList(),
|
|
||||||
child: title,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1779,5 +1797,19 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (widget.onNavigationNotification != null) {
|
||||||
|
child = NotificationListener<NavigationNotification>(
|
||||||
|
onNotification: widget.onNavigationNotification,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return RootRestorationScope(
|
||||||
|
restorationId: widget.restorationScopeId,
|
||||||
|
child: SharedAppData(
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,9 +54,8 @@ export 'dart:ui' show AppLifecycleState, Locale;
|
|||||||
/// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart **
|
/// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart **
|
||||||
/// {@end-tool}
|
/// {@end-tool}
|
||||||
abstract mixin class WidgetsBindingObserver {
|
abstract mixin class WidgetsBindingObserver {
|
||||||
/// Called when the system tells the app to pop the current route.
|
/// Called when the system tells the app to pop the current route, such as
|
||||||
/// For example, on Android, this is called when the user presses
|
/// after a system back button press or back gesture.
|
||||||
/// the back button.
|
|
||||||
///
|
///
|
||||||
/// Observers are notified in registration order until one returns
|
/// Observers are notified in registration order until one returns
|
||||||
/// true. If none return true, the application quits.
|
/// true. If none return true, the application quits.
|
||||||
@ -69,6 +68,8 @@ abstract mixin class WidgetsBindingObserver {
|
|||||||
///
|
///
|
||||||
/// This method exposes the `popRoute` notification from
|
/// This method exposes the `popRoute` notification from
|
||||||
/// [SystemChannels.navigation].
|
/// [SystemChannels.navigation].
|
||||||
|
///
|
||||||
|
/// {@macro flutter.widgets.AndroidPredictiveBack}
|
||||||
Future<bool> didPopRoute() => Future<bool>.value(false);
|
Future<bool> didPopRoute() => Future<bool>.value(false);
|
||||||
|
|
||||||
/// Called when the host tells the application to push a new route onto the
|
/// Called when the host tells the application to push a new route onto the
|
||||||
@ -720,6 +721,27 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB
|
|||||||
///
|
///
|
||||||
/// This method exposes the `popRoute` notification from
|
/// This method exposes the `popRoute` notification from
|
||||||
/// [SystemChannels.navigation].
|
/// [SystemChannels.navigation].
|
||||||
|
///
|
||||||
|
/// {@template flutter.widgets.AndroidPredictiveBack}
|
||||||
|
/// ## Handling backs ahead of time
|
||||||
|
///
|
||||||
|
/// Not all system backs will result in a call to this method. Some are
|
||||||
|
/// handled entirely by the system without informing the Flutter framework.
|
||||||
|
///
|
||||||
|
/// Android API 33+ introduced a feature called predictive back, which allows
|
||||||
|
/// the user to peek behind the current app or route during a back gesture and
|
||||||
|
/// then decide to cancel or commit the back. Flutter enables or disables this
|
||||||
|
/// feature ahead of time, before a back gesture occurs, and back gestures
|
||||||
|
/// that trigger predictive back are handled entirely by the system and do not
|
||||||
|
/// trigger this method here in the framework.
|
||||||
|
///
|
||||||
|
/// By default, the framework communicates when it would like to handle system
|
||||||
|
/// back gestures using [SystemNavigator.setFrameworkHandlesBack] in
|
||||||
|
/// [WidgetsApp.defaultOnNavigationNotification]. This is done automatically
|
||||||
|
/// based on the status of the [Navigator] stack and the state of any
|
||||||
|
/// [PopScope] widgets present. Developers can manually set this by calling
|
||||||
|
/// the method directly or by using [NavigationNotification].
|
||||||
|
/// {@endtemplate}
|
||||||
@protected
|
@protected
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
Future<void> handlePopRoute() async {
|
Future<void> handlePopRoute() async {
|
||||||
|
@ -10,8 +10,10 @@ import 'package:flutter/rendering.dart';
|
|||||||
import 'basic.dart';
|
import 'basic.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'navigator.dart';
|
import 'navigator.dart';
|
||||||
|
import 'pop_scope.dart';
|
||||||
import 'restoration.dart';
|
import 'restoration.dart';
|
||||||
import 'restoration_properties.dart';
|
import 'restoration_properties.dart';
|
||||||
|
import 'routes.dart';
|
||||||
import 'will_pop_scope.dart';
|
import 'will_pop_scope.dart';
|
||||||
|
|
||||||
// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
|
// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
|
||||||
@ -52,10 +54,17 @@ class Form extends StatefulWidget {
|
|||||||
const Form({
|
const Form({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
this.canPop,
|
||||||
|
this.onPopInvoked,
|
||||||
|
@Deprecated(
|
||||||
|
'Use canPop and/or onPopInvoked instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
this.onWillPop,
|
this.onWillPop,
|
||||||
this.onChanged,
|
this.onChanged,
|
||||||
AutovalidateMode? autovalidateMode,
|
AutovalidateMode? autovalidateMode,
|
||||||
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
|
}) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled,
|
||||||
|
assert((onPopInvoked == null && canPop == null) || onWillPop == null, 'onWillPop is deprecated; use canPop and/or onPopInvoked.');
|
||||||
|
|
||||||
/// Returns the [FormState] of the closest [Form] widget which encloses the
|
/// Returns the [FormState] of the closest [Form] widget which encloses the
|
||||||
/// given context, or null if none is found.
|
/// given context, or null if none is found.
|
||||||
@ -134,8 +143,44 @@ class Form extends StatefulWidget {
|
|||||||
///
|
///
|
||||||
/// * [WillPopScope], another widget that provides a way to intercept the
|
/// * [WillPopScope], another widget that provides a way to intercept the
|
||||||
/// back button.
|
/// back button.
|
||||||
|
@Deprecated(
|
||||||
|
'Use canPop and/or onPopInvoked instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
final WillPopCallback? onWillPop;
|
final WillPopCallback? onWillPop;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.PopScope.canPop}
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample demonstrates how to use this parameter to show a confirmation
|
||||||
|
/// dialog when a navigation pop would cause form data to be lost.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/widgets/form/form.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [onPopInvoked], which also comes from [PopScope] and is often used in
|
||||||
|
/// conjunction with this parameter.
|
||||||
|
/// * [PopScope.canPop], which is what [Form] delegates to internally.
|
||||||
|
final bool? canPop;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.navigator.onPopInvoked}
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample demonstrates how to use this parameter to show a confirmation
|
||||||
|
/// dialog when a navigation pop would cause form data to be lost.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/widgets/form/form.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [canPop], which also comes from [PopScope] and is often used in
|
||||||
|
/// conjunction with this parameter.
|
||||||
|
/// * [PopScope.onPopInvoked], which is what [Form] delegates to internally.
|
||||||
|
final PopInvokedCallback? onPopInvoked;
|
||||||
|
|
||||||
/// Called when one of the form fields changes.
|
/// Called when one of the form fields changes.
|
||||||
///
|
///
|
||||||
/// In addition to this callback being invoked, all the form fields themselves
|
/// In addition to this callback being invoked, all the form fields themselves
|
||||||
@ -200,6 +245,18 @@ class FormState extends State<Form> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.canPop != null || widget.onPopInvoked != null) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: widget.canPop ?? true,
|
||||||
|
onPopInvoked: widget.onPopInvoked,
|
||||||
|
child: _FormScope(
|
||||||
|
formState: this,
|
||||||
|
generation: _generation,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: widget.onWillPop,
|
onWillPop: widget.onWillPop,
|
||||||
child: _FormScope(
|
child: _FormScope(
|
||||||
|
@ -20,6 +20,7 @@ import 'focus_scope.dart';
|
|||||||
import 'focus_traversal.dart';
|
import 'focus_traversal.dart';
|
||||||
import 'framework.dart';
|
import 'framework.dart';
|
||||||
import 'heroes.dart';
|
import 'heroes.dart';
|
||||||
|
import 'notification_listener.dart';
|
||||||
import 'overlay.dart';
|
import 'overlay.dart';
|
||||||
import 'restoration.dart';
|
import 'restoration.dart';
|
||||||
import 'restoration_properties.dart';
|
import 'restoration_properties.dart';
|
||||||
@ -67,6 +68,10 @@ typedef RoutePredicate = bool Function(Route<dynamic> route);
|
|||||||
///
|
///
|
||||||
/// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback],
|
/// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback],
|
||||||
/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope].
|
/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope].
|
||||||
|
@Deprecated(
|
||||||
|
'Use PopInvokedCallback instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
typedef WillPopCallback = Future<bool> Function();
|
typedef WillPopCallback = Future<bool> Function();
|
||||||
|
|
||||||
/// Signature for the [Navigator.onPopPage] callback.
|
/// Signature for the [Navigator.onPopPage] callback.
|
||||||
@ -89,19 +94,21 @@ typedef PopPageCallback = bool Function(Route<dynamic> route, dynamic result);
|
|||||||
enum RoutePopDisposition {
|
enum RoutePopDisposition {
|
||||||
/// Pop the route.
|
/// Pop the route.
|
||||||
///
|
///
|
||||||
/// If [Route.willPop] returns [pop] then the back button will actually pop
|
/// If [Route.willPop] or [Route.popDisposition] return [pop] then the back
|
||||||
/// the current route.
|
/// button will actually pop the current route.
|
||||||
pop,
|
pop,
|
||||||
|
|
||||||
/// Do not pop the route.
|
/// Do not pop the route.
|
||||||
///
|
///
|
||||||
/// If [Route.willPop] returns [doNotPop] then the back button will be ignored.
|
/// If [Route.willPop] or [Route.popDisposition] return [doNotPop] then the
|
||||||
|
/// back button will be ignored.
|
||||||
doNotPop,
|
doNotPop,
|
||||||
|
|
||||||
/// Delegate this to the next level of navigation.
|
/// Delegate this to the next level of navigation.
|
||||||
///
|
///
|
||||||
/// If [Route.willPop] returns [bubble] then the back button will be handled
|
/// If [Route.willPop] or [Route.popDisposition] return [bubble] then the back
|
||||||
/// by the [SystemNavigator], which will usually close the application.
|
/// button will be handled by the [SystemNavigator], which will usually close
|
||||||
|
/// the application.
|
||||||
bubble,
|
bubble,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -294,10 +301,51 @@ abstract class Route<T> {
|
|||||||
/// mechanism.
|
/// mechanism.
|
||||||
/// * [WillPopScope], another widget that provides a way to intercept the
|
/// * [WillPopScope], another widget that provides a way to intercept the
|
||||||
/// back button.
|
/// back button.
|
||||||
|
@Deprecated(
|
||||||
|
'Use popDisposition instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
Future<RoutePopDisposition> willPop() async {
|
Future<RoutePopDisposition> willPop() async {
|
||||||
return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
|
return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether calling [Navigator.maybePop] when this [Route] is current
|
||||||
|
/// ([isCurrent]) should do anything.
|
||||||
|
///
|
||||||
|
/// [Navigator.maybePop] is usually used instead of [Navigator.pop] to handle
|
||||||
|
/// the system back button, when it hasn't been disabled via
|
||||||
|
/// [SystemNavigator.setFrameworkHandlesBack].
|
||||||
|
///
|
||||||
|
/// By default, if a [Route] is the first route in the history (i.e., if
|
||||||
|
/// [isFirst]), it reports that pops should be bubbled
|
||||||
|
/// ([RoutePopDisposition.bubble]). This behavior prevents the user from
|
||||||
|
/// popping the first route off the history and being stranded at a blank
|
||||||
|
/// screen; instead, the larger scope is popped (e.g. the application quits,
|
||||||
|
/// so that the user returns to the previous application).
|
||||||
|
///
|
||||||
|
/// In other cases, the default behavior is to accept the pop
|
||||||
|
/// ([RoutePopDisposition.pop]).
|
||||||
|
///
|
||||||
|
/// The third possible value is [RoutePopDisposition.doNotPop], which causes
|
||||||
|
/// the pop request to be ignored entirely.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Form], which provides a [Form.canPop] boolean that is similar.
|
||||||
|
/// * [PopScope], a widget that provides a way to intercept the back button.
|
||||||
|
RoutePopDisposition get popDisposition {
|
||||||
|
return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.navigator.onPopInvoked}
|
||||||
|
/// Called after a route pop was handled.
|
||||||
|
///
|
||||||
|
/// Even when the pop is canceled, for example by a [PopScope] widget, this
|
||||||
|
/// will still be called. The `didPop` parameter indicates whether or not the
|
||||||
|
/// back navigation actually happened successfully.
|
||||||
|
/// {@endtemplate}
|
||||||
|
void onPopInvoked(bool didPop) {}
|
||||||
|
|
||||||
/// Whether calling [didPop] would return false.
|
/// Whether calling [didPop] would return false.
|
||||||
bool get willHandlePopInternally => false;
|
bool get willHandlePopInternally => false;
|
||||||
|
|
||||||
@ -2415,6 +2463,9 @@ class Navigator extends StatefulWidget {
|
|||||||
/// the initial route.
|
/// the initial route.
|
||||||
///
|
///
|
||||||
/// If there is no [Navigator] in scope, returns false.
|
/// If there is no [Navigator] in scope, returns false.
|
||||||
|
///
|
||||||
|
/// Does not consider anything that might externally prevent popping, such as
|
||||||
|
/// [PopEntry].
|
||||||
/// {@endtemplate}
|
/// {@endtemplate}
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
@ -2426,21 +2477,22 @@ class Navigator extends StatefulWidget {
|
|||||||
return navigator != null && navigator.canPop();
|
return navigator != null && navigator.canPop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consults the current route's [Route.willPop] method, and acts accordingly,
|
/// Consults the current route's [Route.popDisposition] getter or
|
||||||
/// potentially popping the route as a result; returns whether the pop request
|
/// [Route.willPop] method, and acts accordingly, potentially popping the
|
||||||
/// should be considered handled.
|
/// route as a result; returns whether the pop request should be considered
|
||||||
|
/// handled.
|
||||||
///
|
///
|
||||||
/// {@template flutter.widgets.navigator.maybePop}
|
/// {@template flutter.widgets.navigator.maybePop}
|
||||||
/// If [Route.willPop] returns [RoutePopDisposition.pop], then the [pop]
|
/// If the [RoutePopDisposition] is [RoutePopDisposition.pop], then the [pop]
|
||||||
/// method is called, and this method returns true, indicating that it handled
|
/// method is called, and this method returns true, indicating that it handled
|
||||||
/// the pop request.
|
/// the pop request.
|
||||||
///
|
///
|
||||||
/// If [Route.willPop] returns [RoutePopDisposition.doNotPop], then this
|
/// If the [RoutePopDisposition] is [RoutePopDisposition.doNotPop], then this
|
||||||
/// method returns true, but does not do anything beyond that.
|
/// method returns true, but does not do anything beyond that.
|
||||||
///
|
///
|
||||||
/// If [Route.willPop] returns [RoutePopDisposition.bubble], then this method
|
/// If the [RoutePopDisposition] is [RoutePopDisposition.bubble], then this
|
||||||
/// returns false, and the caller is responsible for sending the request to
|
/// method returns false, and the caller is responsible for sending the
|
||||||
/// the containing scope (e.g. by closing the application).
|
/// request to the containing scope (e.g. by closing the application).
|
||||||
///
|
///
|
||||||
/// This method is typically called for a user-initiated [pop]. For example on
|
/// This method is typically called for a user-initiated [pop]. For example on
|
||||||
/// Android it's called by the binding for the system's back button.
|
/// Android it's called by the binding for the system's back button.
|
||||||
@ -3015,6 +3067,7 @@ class _RouteEntry extends RouteTransitionRecord {
|
|||||||
assert(isPresent);
|
assert(isPresent);
|
||||||
pendingResult = result;
|
pendingResult = result;
|
||||||
currentState = _RouteLifecycle.pop;
|
currentState = _RouteLifecycle.pop;
|
||||||
|
route.onPopInvoked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _reportRemovalToObserver = true;
|
bool _reportRemovalToObserver = true;
|
||||||
@ -3295,12 +3348,93 @@ class _NavigatorReplaceObservation extends _NavigatorObservation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
typedef _IndexWhereCallback = bool Function(_RouteEntry element);
|
||||||
|
|
||||||
|
/// A collection of _RouteEntries representing a navigation history.
|
||||||
|
///
|
||||||
|
/// Acts as a ChangeNotifier and notifies after its List of _RouteEntries is
|
||||||
|
/// mutated.
|
||||||
|
class _History extends Iterable<_RouteEntry> with ChangeNotifier implements Iterator<_RouteEntry> {
|
||||||
|
final List<_RouteEntry> _value = <_RouteEntry>[];
|
||||||
|
|
||||||
|
int indexWhere(_IndexWhereCallback test, [int start = 0]) {
|
||||||
|
return _value.indexWhere(test, start);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add(_RouteEntry element) {
|
||||||
|
_value.add(element);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addAll(Iterable<_RouteEntry> elements) {
|
||||||
|
_value.addAll(elements);
|
||||||
|
if (elements.isNotEmpty) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
final bool valueWasEmpty = _value.isEmpty;
|
||||||
|
_value.clear();
|
||||||
|
if (!valueWasEmpty) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void insert(int index, _RouteEntry element) {
|
||||||
|
_value.insert(index, element);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
_RouteEntry removeAt(int index) {
|
||||||
|
final _RouteEntry entry = _value.removeAt(index);
|
||||||
|
notifyListeners();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
_RouteEntry removeLast() {
|
||||||
|
final _RouteEntry entry = _value.removeLast();
|
||||||
|
notifyListeners();
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Begin Iterator.
|
||||||
|
|
||||||
|
int _i = 0;
|
||||||
|
|
||||||
|
_RouteEntry operator [](int index) {
|
||||||
|
return _value[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterator<_RouteEntry> get iterator {
|
||||||
|
return _value.iterator;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
_RouteEntry get current => _value[_i];
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool moveNext() {
|
||||||
|
_i++;
|
||||||
|
return _i <= _value.length - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End Iterator.
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return _value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The state for a [Navigator] widget.
|
/// The state for a [Navigator] widget.
|
||||||
///
|
///
|
||||||
/// A reference to this class can be obtained by calling [Navigator.of].
|
/// A reference to this class can be obtained by calling [Navigator.of].
|
||||||
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
|
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
|
||||||
late GlobalKey<OverlayState> _overlayKey;
|
late GlobalKey<OverlayState> _overlayKey;
|
||||||
List<_RouteEntry> _history = <_RouteEntry>[];
|
final _History _history = _History();
|
||||||
|
|
||||||
/// A set for entries that are waiting to dispose until their subtrees are
|
/// A set for entries that are waiting to dispose until their subtrees are
|
||||||
/// disposed.
|
/// disposed.
|
||||||
///
|
///
|
||||||
@ -3330,12 +3464,43 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
|
|
||||||
late List<NavigatorObserver> _effectiveObservers;
|
late List<NavigatorObserver> _effectiveObservers;
|
||||||
|
|
||||||
|
bool get _usingPagesAPI => widget.pages != const <Page<dynamic>>[];
|
||||||
|
|
||||||
|
void _handleHistoryChanged() {
|
||||||
|
final bool navigatorCanPop = canPop();
|
||||||
|
late final bool routeBlocksPop;
|
||||||
|
if (!navigatorCanPop) {
|
||||||
|
final _RouteEntry? lastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
|
||||||
|
routeBlocksPop = lastEntry != null
|
||||||
|
&& lastEntry.route.popDisposition == RoutePopDisposition.doNotPop;
|
||||||
|
} else {
|
||||||
|
routeBlocksPop = false;
|
||||||
|
}
|
||||||
|
final NavigationNotification notification = NavigationNotification(
|
||||||
|
canHandlePop: navigatorCanPop || routeBlocksPop,
|
||||||
|
);
|
||||||
|
// Avoid dispatching a notification in the middle of a build.
|
||||||
|
switch (SchedulerBinding.instance.schedulerPhase) {
|
||||||
|
case SchedulerPhase.postFrameCallbacks:
|
||||||
|
notification.dispatch(context);
|
||||||
|
case SchedulerPhase.idle:
|
||||||
|
case SchedulerPhase.midFrameMicrotasks:
|
||||||
|
case SchedulerPhase.persistentCallbacks:
|
||||||
|
case SchedulerPhase.transientCallbacks:
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||||
|
if (!mounted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notification.dispatch(context);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
assert(() {
|
assert(() {
|
||||||
if (widget.pages != const <Page<dynamic>>[]) {
|
if (_usingPagesAPI) {
|
||||||
// This navigator uses page API.
|
|
||||||
if (widget.pages.isEmpty) {
|
if (widget.pages.isEmpty) {
|
||||||
FlutterError.reportError(
|
FlutterError.reportError(
|
||||||
FlutterErrorDetails(
|
FlutterErrorDetails(
|
||||||
@ -3378,6 +3543,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
if (widget.reportsRouteUpdateToEngine) {
|
if (widget.reportsRouteUpdateToEngine) {
|
||||||
SystemNavigator.selectSingleEntryHistory();
|
SystemNavigator.selectSingleEntryHistory();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_history.addListener(_handleHistoryChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use [_nextPagelessRestorationScopeId] to get the next id.
|
// Use [_nextPagelessRestorationScopeId] to get the next id.
|
||||||
@ -3560,7 +3727,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
void didUpdateWidget(Navigator oldWidget) {
|
void didUpdateWidget(Navigator oldWidget) {
|
||||||
super.didUpdateWidget(oldWidget);
|
super.didUpdateWidget(oldWidget);
|
||||||
assert(() {
|
assert(() {
|
||||||
if (widget.pages != const <Page<dynamic>>[]) {
|
if (_usingPagesAPI) {
|
||||||
// This navigator uses page API.
|
// This navigator uses page API.
|
||||||
if (widget.pages.isEmpty) {
|
if (widget.pages.isEmpty) {
|
||||||
FlutterError.reportError(
|
FlutterError.reportError(
|
||||||
@ -3672,6 +3839,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
_rawNextPagelessRestorationScopeId.dispose();
|
_rawNextPagelessRestorationScopeId.dispose();
|
||||||
_serializableHistory.dispose();
|
_serializableHistory.dispose();
|
||||||
userGestureInProgressNotifier.dispose();
|
userGestureInProgressNotifier.dispose();
|
||||||
|
_history.removeListener(_handleHistoryChanged);
|
||||||
|
_history.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
// don't unlock, so that the object becomes unusable
|
// don't unlock, so that the object becomes unusable
|
||||||
assert(_debugLocked);
|
assert(_debugLocked);
|
||||||
@ -3957,7 +4126,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes,
|
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes,
|
||||||
).cast<_RouteEntry>();
|
).cast<_RouteEntry>();
|
||||||
}
|
}
|
||||||
_history = <_RouteEntry>[];
|
_history.clear();
|
||||||
// Adds the leading pageless routes if there is any.
|
// Adds the leading pageless routes if there is any.
|
||||||
if (pageRouteToPagelessRoutes.containsKey(null)) {
|
if (pageRouteToPagelessRoutes.containsKey(null)) {
|
||||||
_history.addAll(pageRouteToPagelessRoutes[null]!);
|
_history.addAll(pageRouteToPagelessRoutes[null]!);
|
||||||
@ -4973,17 +5142,17 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
return true; // there's at least two routes, so we can pop
|
return true; // there's at least two routes, so we can pop
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Consults the current route's [Route.willPop] method, and acts accordingly,
|
/// Consults the current route's [Route.popDisposition] method, and acts
|
||||||
/// potentially popping the route as a result; returns whether the pop request
|
/// accordingly, potentially popping the route as a result; returns whether
|
||||||
/// should be considered handled.
|
/// the pop request should be considered handled.
|
||||||
///
|
///
|
||||||
/// {@macro flutter.widgets.navigator.maybePop}
|
/// {@macro flutter.widgets.navigator.maybePop}
|
||||||
///
|
///
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [Form], which provides an `onWillPop` callback that enables the form
|
/// * [Form], which provides a [Form.canPop] boolean that enables the
|
||||||
/// to veto a [pop] initiated by the app's back button.
|
/// form to prevent any [pop]s initiated by the app's back button.
|
||||||
/// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used
|
/// * [ModalRoute], which provides a `scopedOnPopCallback` that can be used
|
||||||
/// to define the route's `willPop` method.
|
/// to define the route's `willPop` method.
|
||||||
@optionalTypeArgs
|
@optionalTypeArgs
|
||||||
Future<bool> maybePop<T extends Object?>([ T? result ]) async {
|
Future<bool> maybePop<T extends Object?>([ T? result ]) async {
|
||||||
@ -4992,23 +5161,31 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
assert(lastEntry.route._navigator == this);
|
assert(lastEntry.route._navigator == this);
|
||||||
final RoutePopDisposition disposition = await lastEntry.route.willPop(); // this is asynchronous
|
|
||||||
|
// TODO(justinmc): When the deprecated willPop method is removed, delete
|
||||||
|
// this code and use only popDisposition, below.
|
||||||
|
final RoutePopDisposition willPopDisposition = await lastEntry.route.willPop();
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
// Forget about this pop, we were disposed in the meantime.
|
// Forget about this pop, we were disposed in the meantime.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (willPopDisposition == RoutePopDisposition.doNotPop) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
final _RouteEntry? newLastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
|
final _RouteEntry? newLastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
|
||||||
if (lastEntry != newLastEntry) {
|
if (lastEntry != newLastEntry) {
|
||||||
// Forget about this pop, something happened to our history in the meantime.
|
// Forget about this pop, something happened to our history in the meantime.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
switch (disposition) {
|
|
||||||
|
switch (lastEntry.route.popDisposition) {
|
||||||
case RoutePopDisposition.bubble:
|
case RoutePopDisposition.bubble:
|
||||||
return false;
|
return false;
|
||||||
case RoutePopDisposition.pop:
|
case RoutePopDisposition.pop:
|
||||||
pop(result);
|
pop(result);
|
||||||
return true;
|
return true;
|
||||||
case RoutePopDisposition.doNotPop:
|
case RoutePopDisposition.doNotPop:
|
||||||
|
lastEntry.route.onPopInvoked(false);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5298,29 +5475,46 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(!_debugLocked);
|
assert(!_debugLocked);
|
||||||
assert(_history.isNotEmpty);
|
assert(_history.isNotEmpty);
|
||||||
|
|
||||||
// Hides the HeroControllerScope for the widget subtree so that the other
|
// Hides the HeroControllerScope for the widget subtree so that the other
|
||||||
// nested navigator underneath will not pick up the hero controller above
|
// nested navigator underneath will not pick up the hero controller above
|
||||||
// this level.
|
// this level.
|
||||||
return HeroControllerScope.none(
|
return HeroControllerScope.none(
|
||||||
child: Listener(
|
child: NotificationListener<NavigationNotification>(
|
||||||
onPointerDown: _handlePointerDown,
|
onNotification: (NavigationNotification notification) {
|
||||||
onPointerUp: _handlePointerUpOrCancel,
|
// If the state of this Navigator does not change whether or not the
|
||||||
onPointerCancel: _handlePointerUpOrCancel,
|
// whole framework can pop, propagate the Notification as-is.
|
||||||
child: AbsorbPointer(
|
if (notification.canHandlePop || !canPop()) {
|
||||||
absorbing: false, // it's mutated directly by _cancelActivePointers above
|
return false;
|
||||||
child: FocusTraversalGroup(
|
}
|
||||||
policy: FocusTraversalGroup.maybeOf(context),
|
// Otherwise, dispatch a new Notification with the correct canPop and
|
||||||
child: Focus(
|
// stop the propagation of the old Notification.
|
||||||
focusNode: focusNode,
|
const NavigationNotification nextNotification = NavigationNotification(
|
||||||
autofocus: true,
|
canHandlePop: true,
|
||||||
skipTraversal: true,
|
);
|
||||||
includeSemantics: false,
|
nextNotification.dispatch(context);
|
||||||
child: UnmanagedRestorationScope(
|
return true;
|
||||||
bucket: bucket,
|
},
|
||||||
child: Overlay(
|
child: Listener(
|
||||||
key: _overlayKey,
|
onPointerDown: _handlePointerDown,
|
||||||
clipBehavior: widget.clipBehavior,
|
onPointerUp: _handlePointerUpOrCancel,
|
||||||
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
|
onPointerCancel: _handlePointerUpOrCancel,
|
||||||
|
child: AbsorbPointer(
|
||||||
|
absorbing: false, // it's mutated directly by _cancelActivePointers above
|
||||||
|
child: FocusTraversalGroup(
|
||||||
|
policy: FocusTraversalGroup.maybeOf(context),
|
||||||
|
child: Focus(
|
||||||
|
focusNode: focusNode,
|
||||||
|
autofocus: true,
|
||||||
|
skipTraversal: true,
|
||||||
|
includeSemantics: false,
|
||||||
|
child: UnmanagedRestorationScope(
|
||||||
|
bucket: bucket,
|
||||||
|
child: Overlay(
|
||||||
|
key: _overlayKey,
|
||||||
|
clipBehavior: widget.clipBehavior,
|
||||||
|
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -5481,7 +5675,7 @@ class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
|
|||||||
|
|
||||||
// Updating.
|
// Updating.
|
||||||
|
|
||||||
void update(List<_RouteEntry> history) {
|
void update(_History history) {
|
||||||
assert(isRegistered);
|
assert(isRegistered);
|
||||||
final bool wasUninitialized = _pageToPagelessRoutes == null;
|
final bool wasUninitialized = _pageToPagelessRoutes == null;
|
||||||
bool needsSerialization = wasUninitialized;
|
bool needsSerialization = wasUninitialized;
|
||||||
@ -5804,3 +5998,26 @@ class RestorableRouteFuture<T> extends RestorableProperty<String?> {
|
|||||||
|
|
||||||
static NavigatorState _defaultNavigatorFinder(BuildContext context) => Navigator.of(context);
|
static NavigatorState _defaultNavigatorFinder(BuildContext context) => Navigator.of(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A notification that a change in navigation has taken place.
|
||||||
|
///
|
||||||
|
/// Specifically, this notification indicates that at least one of the following
|
||||||
|
/// has occurred:
|
||||||
|
///
|
||||||
|
/// * That route stack of a [Navigator] has changed in any way.
|
||||||
|
/// * The ability to pop has changed, such as controlled by [PopScope].
|
||||||
|
class NavigationNotification extends Notification {
|
||||||
|
/// Creates a notification that some change in navigation has happened.
|
||||||
|
const NavigationNotification({
|
||||||
|
required this.canHandlePop,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Indicates that the originator of this [Notification] is capable of
|
||||||
|
/// handling a navigation pop.
|
||||||
|
final bool canHandlePop;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'NavigationNotification canHandlePop: $canHandlePop';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
110
packages/flutter/lib/src/widgets/navigator_pop_handler.dart
Normal file
110
packages/flutter/lib/src/widgets/navigator_pop_handler.dart
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'framework.dart';
|
||||||
|
import 'navigator.dart';
|
||||||
|
import 'notification_listener.dart';
|
||||||
|
import 'pop_scope.dart';
|
||||||
|
|
||||||
|
/// Enables the handling of system back gestures.
|
||||||
|
///
|
||||||
|
/// Typically wraps a nested [Navigator] widget and allows it to handle system
|
||||||
|
/// back gestures in the [onPop] callback.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample demonstrates how to use this widget to properly handle system
|
||||||
|
/// back gestures when using nested [Navigator]s.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample demonstrates how to use this widget to properly handle system
|
||||||
|
/// back gestures with a bottom navigation bar whose tabs each have their own
|
||||||
|
/// nested [Navigator]s.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [PopScope], which allows toggling the ability of a [Navigator] to
|
||||||
|
/// handle pops.
|
||||||
|
/// * [NavigationNotification], which indicates whether a [Navigator] in a
|
||||||
|
/// subtree can handle pops.
|
||||||
|
class NavigatorPopHandler extends StatefulWidget {
|
||||||
|
/// Creates an instance of [NavigatorPopHandler].
|
||||||
|
const NavigatorPopHandler({
|
||||||
|
super.key,
|
||||||
|
this.onPop,
|
||||||
|
this.enabled = true,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The widget to place below this in the widget tree.
|
||||||
|
///
|
||||||
|
/// Typically this is a [Navigator] that will handle the pop when [onPop] is
|
||||||
|
/// called.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Whether this widget's ability to handle system back gestures is enabled or
|
||||||
|
/// disabled.
|
||||||
|
///
|
||||||
|
/// When false, there will be no effect on system back gestures. If provided,
|
||||||
|
/// [onPop] will still be called.
|
||||||
|
///
|
||||||
|
/// This can be used, for example, when the nested [Navigator] is no longer
|
||||||
|
/// active but remains in the widget tree, such as in an inactive tab.
|
||||||
|
///
|
||||||
|
/// Defaults to true.
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
/// Called when a handleable pop event happens.
|
||||||
|
///
|
||||||
|
/// For example, a pop is handleable when a [Navigator] in [child] has
|
||||||
|
/// multiple routes on its stack. It's not handleable when it has only a
|
||||||
|
/// single route, and so [onPop] will not be called.
|
||||||
|
///
|
||||||
|
/// Typically this is used to pop the [Navigator] in [child]. See the sample
|
||||||
|
/// code on [NavigatorPopHandler] for a full example of this.
|
||||||
|
final VoidCallback? onPop;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<NavigatorPopHandler> createState() => _NavigatorPopHandlerState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NavigatorPopHandlerState extends State<NavigatorPopHandler> {
|
||||||
|
bool _canPop = true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// When the widget subtree indicates it can handle a pop, disable popping
|
||||||
|
// here, so that it can be manually handled in canPop.
|
||||||
|
return PopScope(
|
||||||
|
canPop: !widget.enabled || _canPop,
|
||||||
|
onPopInvoked: (bool didPop) {
|
||||||
|
if (didPop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
widget.onPop?.call();
|
||||||
|
},
|
||||||
|
// Listen to changes in the navigation stack in the widget subtree.
|
||||||
|
child: NotificationListener<NavigationNotification>(
|
||||||
|
onNotification: (NavigationNotification notification) {
|
||||||
|
// If this subtree cannot handle pop, then set canPop to true so
|
||||||
|
// that our PopScope will allow the Navigator higher in the tree to
|
||||||
|
// handle the pop instead.
|
||||||
|
final bool nextCanPop = !notification.canHandlePop;
|
||||||
|
if (nextCanPop != _canPop) {
|
||||||
|
setState(() {
|
||||||
|
_canPop = nextCanPop;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
137
packages/flutter/lib/src/widgets/pop_scope.dart
Normal file
137
packages/flutter/lib/src/widgets/pop_scope.dart
Normal file
@ -0,0 +1,137 @@
|
|||||||
|
// 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/foundation.dart';
|
||||||
|
|
||||||
|
import 'framework.dart';
|
||||||
|
import 'navigator.dart';
|
||||||
|
import 'routes.dart';
|
||||||
|
|
||||||
|
/// Manages system back gestures.
|
||||||
|
///
|
||||||
|
/// The [canPop] parameter can be used to disable system back gestures. Defaults
|
||||||
|
/// to true, meaning that back gestures happen as usual.
|
||||||
|
///
|
||||||
|
/// The [onPopInvoked] parameter reports when system back gestures occur,
|
||||||
|
/// regardless of whether or not they were successful.
|
||||||
|
///
|
||||||
|
/// If [canPop] is false, then a system back gesture will not pop the route off
|
||||||
|
/// of the enclosing [Navigator]. [onPopInvoked] will still be called, and
|
||||||
|
/// `didPop` will be `false`.
|
||||||
|
///
|
||||||
|
/// If [canPop] is true, then a system back gesture will cause the enclosing
|
||||||
|
/// [Navigator] to receive a pop as usual. [onPopInvoked] will be called with
|
||||||
|
/// `didPop` as `true`, unless the pop failed for reasons unrelated to
|
||||||
|
/// [PopScope], in which case it will be `false`.
|
||||||
|
///
|
||||||
|
/// {@tool dartpad}
|
||||||
|
/// This sample demonstrates how to use this widget to handle nested navigation
|
||||||
|
/// in a bottom navigation bar.
|
||||||
|
///
|
||||||
|
/// ** See code in examples/api/lib/widgets/pop_scope/pop_scope.0.dart **
|
||||||
|
/// {@end-tool}
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [NavigatorPopHandler], which is a less verbose way to handle system back
|
||||||
|
/// gestures in simple cases of nested [Navigator]s.
|
||||||
|
/// * [Form.canPop] and [Form.onPopInvoked], which can be used to handle system
|
||||||
|
/// back gestures in the case of a form with unsaved data.
|
||||||
|
/// * [ModalRoute.registerPopEntry] and [ModalRoute.unregisterPopEntry],
|
||||||
|
/// which this widget uses to integrate with Flutter's navigation system.
|
||||||
|
class PopScope extends StatefulWidget {
|
||||||
|
/// Creates a widget that registers a callback to veto attempts by the user to
|
||||||
|
/// dismiss the enclosing [ModalRoute].
|
||||||
|
const PopScope({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.canPop = true,
|
||||||
|
this.onPopInvoked,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// The widget below this widget in the tree.
|
||||||
|
///
|
||||||
|
/// {@macro flutter.widgets.ProxyWidget.child}
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.PopScope.onPopInvoked}
|
||||||
|
/// Called after a route pop was handled.
|
||||||
|
/// {@endtemplate}
|
||||||
|
///
|
||||||
|
/// It's not possible to prevent the pop from happening at the time that this
|
||||||
|
/// method is called; the pop has already happened. Use [canPop] to
|
||||||
|
/// disable pops in advance.
|
||||||
|
///
|
||||||
|
/// This will still be called even when the pop is canceled. A pop is canceled
|
||||||
|
/// when the relevant [Route.popDisposition] returns false, such as when
|
||||||
|
/// [canPop] is set to false on a [PopScope]. The `didPop` parameter
|
||||||
|
/// indicates whether or not the back navigation actually happened
|
||||||
|
/// successfully.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Route.onPopInvoked], which is similar.
|
||||||
|
final PopInvokedCallback? onPopInvoked;
|
||||||
|
|
||||||
|
/// {@template flutter.widgets.PopScope.canPop}
|
||||||
|
/// When false, blocks the current route from being popped.
|
||||||
|
///
|
||||||
|
/// This includes the root route, where upon popping, the Flutter app would
|
||||||
|
/// exit.
|
||||||
|
///
|
||||||
|
/// If multiple [PopScope] widgets appear in a route's widget subtree, then
|
||||||
|
/// each and every `canPop` must be `true` in order for the route to be
|
||||||
|
/// able to pop.
|
||||||
|
///
|
||||||
|
/// [Android's predictive back](https://developer.android.com/guide/navigation/predictive-back-gesture)
|
||||||
|
/// feature will not animate when this boolean is false.
|
||||||
|
/// {@endtemplate}
|
||||||
|
final bool canPop;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PopScope> createState() => _PopScopeState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopScopeState extends State<PopScope> implements PopEntry {
|
||||||
|
ModalRoute<dynamic>? _route;
|
||||||
|
|
||||||
|
@override
|
||||||
|
PopInvokedCallback? get onPopInvoked => widget.onPopInvoked;
|
||||||
|
|
||||||
|
@override
|
||||||
|
late final ValueNotifier<bool> canPopNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
canPopNotifier = ValueNotifier<bool>(widget.canPop);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final ModalRoute<dynamic>? nextRoute = ModalRoute.of(context);
|
||||||
|
if (nextRoute != _route) {
|
||||||
|
_route?.unregisterPopEntry(this);
|
||||||
|
_route = nextRoute;
|
||||||
|
_route?.registerPopEntry(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(PopScope oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
canPopNotifier.value = widget.canPop;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_route?.unregisterPopEntry(this);
|
||||||
|
canPopNotifier.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => widget.child;
|
||||||
|
}
|
@ -717,6 +717,10 @@ mixin LocalHistoryRoute<T> on Route<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated(
|
||||||
|
'Use popDisposition instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
Future<RoutePopDisposition> willPop() async {
|
Future<RoutePopDisposition> willPop() async {
|
||||||
if (willHandlePopInternally) {
|
if (willHandlePopInternally) {
|
||||||
@ -725,6 +729,14 @@ mixin LocalHistoryRoute<T> on Route<T> {
|
|||||||
return super.willPop();
|
return super.willPop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
RoutePopDisposition get popDisposition {
|
||||||
|
if (willHandlePopInternally) {
|
||||||
|
return RoutePopDisposition.pop;
|
||||||
|
}
|
||||||
|
return super.popDisposition;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool didPop(T? result) {
|
bool didPop(T? result) {
|
||||||
if (_localHistory != null && _localHistory!.isNotEmpty) {
|
if (_localHistory != null && _localHistory!.isNotEmpty) {
|
||||||
@ -1490,6 +1502,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
|
|
||||||
final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
|
final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
|
||||||
|
|
||||||
|
final Set<PopEntry> _popEntries = <PopEntry>{};
|
||||||
|
|
||||||
/// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
|
/// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
|
||||||
/// [addScopedWillPopCallback] returns either false or null. If they all
|
/// [addScopedWillPopCallback] returns either false or null. If they all
|
||||||
/// return true, the base [Route.willPop]'s result will be returned. The
|
/// return true, the base [Route.willPop]'s result will be returned. The
|
||||||
@ -1508,6 +1522,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
/// method checks.
|
/// method checks.
|
||||||
/// * [removeScopedWillPopCallback], which removes a callback from the list
|
/// * [removeScopedWillPopCallback], which removes a callback from the list
|
||||||
/// this method checks.
|
/// this method checks.
|
||||||
|
@Deprecated(
|
||||||
|
'Use popDisposition instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
@override
|
@override
|
||||||
Future<RoutePopDisposition> willPop() async {
|
Future<RoutePopDisposition> willPop() async {
|
||||||
final _ModalScopeState<T>? scope = _scopeKey.currentState;
|
final _ModalScopeState<T>? scope = _scopeKey.currentState;
|
||||||
@ -1520,26 +1538,44 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
return super.willPop();
|
return super.willPop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns [RoutePopDisposition.doNotPop] if any of the [PopEntry] instances
|
||||||
|
/// registered with [registerPopEntry] have [PopEntry.canPopNotifier] set to
|
||||||
|
/// false.
|
||||||
|
///
|
||||||
|
/// Typically this method is not overridden because applications usually
|
||||||
|
/// don't create modal routes directly, they use higher level primitives
|
||||||
|
/// like [showDialog]. The scoped [PopEntry] list makes it possible for
|
||||||
|
/// ModalRoute descendants to collectively define the value of
|
||||||
|
/// [popDisposition].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [Form], which provides an `onPopInvoked` callback that is similar.
|
||||||
|
/// * [registerPopEntry], which adds a [PopEntry] to the list this method
|
||||||
|
/// checks.
|
||||||
|
/// * [unregisterPopEntry], which removes a [PopEntry] from the list this
|
||||||
|
/// method checks.
|
||||||
|
@override
|
||||||
|
RoutePopDisposition get popDisposition {
|
||||||
|
final bool canPop = _popEntries.every((PopEntry popEntry) {
|
||||||
|
return popEntry.canPopNotifier.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!canPop) {
|
||||||
|
return RoutePopDisposition.doNotPop;
|
||||||
|
}
|
||||||
|
return super.popDisposition;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onPopInvoked(bool didPop) {
|
||||||
|
for (final PopEntry popEntry in _popEntries) {
|
||||||
|
popEntry.onPopInvoked?.call(didPop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Enables this route to veto attempts by the user to dismiss it.
|
/// Enables this route to veto attempts by the user to dismiss it.
|
||||||
///
|
///
|
||||||
/// {@tool snippet}
|
|
||||||
/// This callback is typically added using a [WillPopScope] widget. That
|
|
||||||
/// widget finds the enclosing [ModalRoute] and uses this function to register
|
|
||||||
/// this callback:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// Widget build(BuildContext context) {
|
|
||||||
/// return WillPopScope(
|
|
||||||
/// onWillPop: () async {
|
|
||||||
/// // ask the user if they are sure
|
|
||||||
/// return true;
|
|
||||||
/// },
|
|
||||||
/// child: Container(),
|
|
||||||
/// );
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// This callback runs asynchronously and it's possible that it will be called
|
/// This callback runs asynchronously and it's possible that it will be called
|
||||||
/// after its route has been disposed. The callback should check [State.mounted]
|
/// after its route has been disposed. The callback should check [State.mounted]
|
||||||
/// before doing anything.
|
/// before doing anything.
|
||||||
@ -1548,49 +1584,6 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
/// unsaved [Form] data if the user attempts to back out of the form. In that
|
/// unsaved [Form] data if the user attempts to back out of the form. In that
|
||||||
/// case, use the [Form.onWillPop] property to register the callback.
|
/// case, use the [Form.onWillPop] property to register the callback.
|
||||||
///
|
///
|
||||||
/// {@tool snippet}
|
|
||||||
/// To register a callback manually, look up the enclosing [ModalRoute] in a
|
|
||||||
/// [State.didChangeDependencies] callback:
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// abstract class _MyWidgetState extends State<MyWidget> {
|
|
||||||
/// ModalRoute<dynamic>? _route;
|
|
||||||
///
|
|
||||||
/// // ...
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// void didChangeDependencies() {
|
|
||||||
/// super.didChangeDependencies();
|
|
||||||
/// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
|
|
||||||
/// _route = ModalRoute.of(context);
|
|
||||||
/// _route?.addScopedWillPopCallback(askTheUserIfTheyAreSure);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// {@tool snippet}
|
|
||||||
/// If you register a callback manually, be sure to remove the callback with
|
|
||||||
/// [removeScopedWillPopCallback] by the time the widget has been disposed. A
|
|
||||||
/// stateful widget can do this in its dispose method (continuing the previous
|
|
||||||
/// example):
|
|
||||||
///
|
|
||||||
/// ```dart
|
|
||||||
/// abstract class _MyWidgetState2 extends State<MyWidget> {
|
|
||||||
/// ModalRoute<dynamic>? _route;
|
|
||||||
///
|
|
||||||
/// // ...
|
|
||||||
///
|
|
||||||
/// @override
|
|
||||||
/// void dispose() {
|
|
||||||
/// _route?.removeScopedWillPopCallback(askTheUserIfTheyAreSure);
|
|
||||||
/// _route = null;
|
|
||||||
/// super.dispose();
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [WillPopScope], which manages the registration and unregistration
|
/// * [WillPopScope], which manages the registration and unregistration
|
||||||
@ -1599,6 +1592,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
/// * [willPop], which runs the callbacks added with this method.
|
/// * [willPop], which runs the callbacks added with this method.
|
||||||
/// * [removeScopedWillPopCallback], which removes a callback from the list
|
/// * [removeScopedWillPopCallback], which removes a callback from the list
|
||||||
/// that [willPop] checks.
|
/// that [willPop] checks.
|
||||||
|
@Deprecated(
|
||||||
|
'Use registerPopEntry or PopScope instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
void addScopedWillPopCallback(WillPopCallback callback) {
|
void addScopedWillPopCallback(WillPopCallback callback) {
|
||||||
assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
|
assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
|
||||||
_willPopCallbacks.add(callback);
|
_willPopCallbacks.add(callback);
|
||||||
@ -1611,11 +1608,69 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
/// * [Form], which provides an `onWillPop` callback that uses this mechanism.
|
/// * [Form], which provides an `onWillPop` callback that uses this mechanism.
|
||||||
/// * [addScopedWillPopCallback], which adds callback to the list
|
/// * [addScopedWillPopCallback], which adds callback to the list
|
||||||
/// checked by [willPop].
|
/// checked by [willPop].
|
||||||
|
@Deprecated(
|
||||||
|
'Use unregisterPopEntry or PopScope instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
void removeScopedWillPopCallback(WillPopCallback callback) {
|
void removeScopedWillPopCallback(WillPopCallback callback) {
|
||||||
assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
|
assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
|
||||||
_willPopCallbacks.remove(callback);
|
_willPopCallbacks.remove(callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Registers the existence of a [PopEntry] in the route.
|
||||||
|
///
|
||||||
|
/// [PopEntry] instances registered in this way will have their
|
||||||
|
/// [PopEntry.onPopInvoked] callbacks called when a route is popped or a pop
|
||||||
|
/// is attempted. They will also be able to block pop operations with
|
||||||
|
/// [PopEntry.canPopNotifier] through this route's [popDisposition] method.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [unregisterPopEntry], which performs the opposite operation.
|
||||||
|
void registerPopEntry(PopEntry popEntry) {
|
||||||
|
_popEntries.add(popEntry);
|
||||||
|
popEntry.canPopNotifier.addListener(_handlePopEntryChange);
|
||||||
|
_handlePopEntryChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Unregisters a [PopEntry] in the route's widget subtree.
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [registerPopEntry], which performs the opposite operation.
|
||||||
|
void unregisterPopEntry(PopEntry popEntry) {
|
||||||
|
_popEntries.remove(popEntry);
|
||||||
|
popEntry.canPopNotifier.removeListener(_handlePopEntryChange);
|
||||||
|
_handlePopEntryChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handlePopEntryChange() {
|
||||||
|
if (!isCurrent) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final NavigationNotification notification = NavigationNotification(
|
||||||
|
// canPop indicates that the originator of the Notification can handle a
|
||||||
|
// pop. In the case of PopScope, it handles pops when canPop is
|
||||||
|
// false. Hence the seemingly backward logic here.
|
||||||
|
canHandlePop: popDisposition == RoutePopDisposition.doNotPop,
|
||||||
|
);
|
||||||
|
// Avoid dispatching a notification in the middle of a build.
|
||||||
|
switch (SchedulerBinding.instance.schedulerPhase) {
|
||||||
|
case SchedulerPhase.postFrameCallbacks:
|
||||||
|
notification.dispatch(subtreeContext);
|
||||||
|
case SchedulerPhase.idle:
|
||||||
|
case SchedulerPhase.midFrameMicrotasks:
|
||||||
|
case SchedulerPhase.persistentCallbacks:
|
||||||
|
case SchedulerPhase.transientCallbacks:
|
||||||
|
SchedulerBinding.instance.addPostFrameCallback((Duration timeStamp) {
|
||||||
|
if (!(subtreeContext?.mounted ?? false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
notification.dispatch(subtreeContext);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// True if one or more [WillPopCallback] callbacks exist.
|
/// True if one or more [WillPopCallback] callbacks exist.
|
||||||
///
|
///
|
||||||
/// This method is used to disable the horizontal swipe pop gesture supported
|
/// This method is used to disable the horizontal swipe pop gesture supported
|
||||||
@ -1633,6 +1688,10 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
/// * [removeScopedWillPopCallback], which removes a callback.
|
/// * [removeScopedWillPopCallback], which removes a callback.
|
||||||
/// * [willHandlePopInternally], which reports on another reason why
|
/// * [willHandlePopInternally], which reports on another reason why
|
||||||
/// a pop might be vetoed.
|
/// a pop might be vetoed.
|
||||||
|
@Deprecated(
|
||||||
|
'Use popDisposition instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
@protected
|
@protected
|
||||||
bool get hasScopedWillPopCallback {
|
bool get hasScopedWillPopCallback {
|
||||||
return _willPopCallbacks.isNotEmpty;
|
return _willPopCallbacks.isNotEmpty;
|
||||||
@ -1772,6 +1831,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get willHandlePopInternally {
|
||||||
|
final bool popEntriesCanPop = _popEntries.every((PopEntry popEntry) {
|
||||||
|
return popEntry.canPopNotifier.value;
|
||||||
|
});
|
||||||
|
return !popEntriesCanPop || super.willHandlePopInternally;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)';
|
String toString() => '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)';
|
||||||
}
|
}
|
||||||
@ -2212,3 +2279,33 @@ typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<doubl
|
|||||||
///
|
///
|
||||||
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
|
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
|
||||||
typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
|
typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
|
||||||
|
|
||||||
|
/// A callback type for informing that a navigation pop has been invoked,
|
||||||
|
/// whether or not it was handled successfully.
|
||||||
|
///
|
||||||
|
/// Accepts a didPop boolean indicating whether or not back navigation
|
||||||
|
/// succeeded.
|
||||||
|
typedef PopInvokedCallback = void Function(bool didPop);
|
||||||
|
|
||||||
|
/// Allows listening to and preventing pops.
|
||||||
|
///
|
||||||
|
/// Can be registered in [ModalRoute] to listen to pops with [onPopInvoked] or
|
||||||
|
/// to enable/disable them with [canPopNotifier].
|
||||||
|
///
|
||||||
|
/// See also:
|
||||||
|
///
|
||||||
|
/// * [PopScope], which provides similar functionality in a widget.
|
||||||
|
/// * [ModalRoute.registerPopEntry], which unregisters instances of this.
|
||||||
|
/// * [ModalRoute.unregisterPopEntry], which unregisters instances of this.
|
||||||
|
abstract class PopEntry {
|
||||||
|
/// {@macro flutter.widgets.PopScope.onPopInvoked}
|
||||||
|
PopInvokedCallback? get onPopInvoked;
|
||||||
|
|
||||||
|
/// {@macro flutter.widgets.PopScope.canPop}
|
||||||
|
ValueListenable<bool> get canPopNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'PopEntry canPop: ${canPopNotifier.value}, onPopInvoked: $onPopInvoked';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,26 +9,25 @@ import 'routes.dart';
|
|||||||
/// Registers a callback to veto attempts by the user to dismiss the enclosing
|
/// Registers a callback to veto attempts by the user to dismiss the enclosing
|
||||||
/// [ModalRoute].
|
/// [ModalRoute].
|
||||||
///
|
///
|
||||||
/// {@tool dartpad}
|
|
||||||
/// Whenever the back button is pressed, you will get a callback at [onWillPop],
|
|
||||||
/// which returns a [Future]. If the [Future] returns true, the screen is
|
|
||||||
/// popped.
|
|
||||||
///
|
|
||||||
/// ** See code in examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart **
|
|
||||||
/// {@end-tool}
|
|
||||||
///
|
|
||||||
/// See also:
|
/// See also:
|
||||||
///
|
///
|
||||||
/// * [ModalRoute.addScopedWillPopCallback] and [ModalRoute.removeScopedWillPopCallback],
|
/// * [ModalRoute.addScopedWillPopCallback] and [ModalRoute.removeScopedWillPopCallback],
|
||||||
/// which this widget uses to register and unregister [onWillPop].
|
/// which this widget uses to register and unregister [onWillPop].
|
||||||
/// * [Form], which provides an `onWillPop` callback that enables the form
|
/// * [Form], which provides an `onWillPop` callback that enables the form
|
||||||
/// to veto a `pop` initiated by the app's back button.
|
/// to veto a `pop` initiated by the app's back button.
|
||||||
///
|
@Deprecated(
|
||||||
|
'Use PopScope instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
class WillPopScope extends StatefulWidget {
|
class WillPopScope extends StatefulWidget {
|
||||||
/// Creates a widget that registers a callback to veto attempts by the user to
|
/// Creates a widget that registers a callback to veto attempts by the user to
|
||||||
/// dismiss the enclosing [ModalRoute].
|
/// dismiss the enclosing [ModalRoute].
|
||||||
///
|
///
|
||||||
/// The [child] argument must not be null.
|
/// The [child] argument must not be null.
|
||||||
|
@Deprecated(
|
||||||
|
'Use PopScope instead. '
|
||||||
|
'This feature was deprecated after v3.12.0-1.0.pre.',
|
||||||
|
)
|
||||||
const WillPopScope({
|
const WillPopScope({
|
||||||
super.key,
|
super.key,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
@ -81,6 +81,7 @@ export 'src/widgets/media_query.dart';
|
|||||||
export 'src/widgets/modal_barrier.dart';
|
export 'src/widgets/modal_barrier.dart';
|
||||||
export 'src/widgets/navigation_toolbar.dart';
|
export 'src/widgets/navigation_toolbar.dart';
|
||||||
export 'src/widgets/navigator.dart';
|
export 'src/widgets/navigator.dart';
|
||||||
|
export 'src/widgets/navigator_pop_handler.dart';
|
||||||
export 'src/widgets/nested_scroll_view.dart';
|
export 'src/widgets/nested_scroll_view.dart';
|
||||||
export 'src/widgets/notification_listener.dart';
|
export 'src/widgets/notification_listener.dart';
|
||||||
export 'src/widgets/orientation_builder.dart';
|
export 'src/widgets/orientation_builder.dart';
|
||||||
@ -95,6 +96,7 @@ export 'src/widgets/placeholder.dart';
|
|||||||
export 'src/widgets/platform_menu_bar.dart';
|
export 'src/widgets/platform_menu_bar.dart';
|
||||||
export 'src/widgets/platform_selectable_region_context_menu.dart';
|
export 'src/widgets/platform_selectable_region_context_menu.dart';
|
||||||
export 'src/widgets/platform_view.dart';
|
export 'src/widgets/platform_view.dart';
|
||||||
|
export 'src/widgets/pop_scope.dart';
|
||||||
export 'src/widgets/preferred_size.dart';
|
export 'src/widgets/preferred_size.dart';
|
||||||
export 'src/widgets/primary_scroll_controller.dart';
|
export 'src/widgets/primary_scroll_controller.dart';
|
||||||
export 'src/widgets/raw_keyboard_listener.dart';
|
export 'src/widgets/raw_keyboard_listener.dart';
|
||||||
|
@ -2,13 +2,14 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import '../image_data.dart';
|
import '../image_data.dart';
|
||||||
import '../rendering/rendering_tester.dart' show TestCallbackPainter;
|
import '../rendering/rendering_tester.dart' show TestCallbackPainter;
|
||||||
|
import '../widgets/navigator_utils.dart';
|
||||||
|
|
||||||
late List<int> selectedTabs;
|
late List<int> selectedTabs;
|
||||||
|
|
||||||
@ -1215,6 +1216,132 @@ void main() {
|
|||||||
expect(find.text('Content 2'), findsNothing);
|
expect(find.text('Content 2'), findsNothing);
|
||||||
expect(find.text('Content 3'), findsNothing);
|
expect(find.text('Content 3'), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Android Predictive Back', () {
|
||||||
|
bool? lastFrameworkHandlesBack;
|
||||||
|
setUp(() {
|
||||||
|
// Initialize to false. Because this uses a static boolean internally, it
|
||||||
|
// is not reset between tests or calls to pumpWidget. Explicitly setting
|
||||||
|
// it to false before each test makes them behave deterministically.
|
||||||
|
SystemNavigator.setFrameworkHandlesBack(false);
|
||||||
|
lastFrameworkHandlesBack = null;
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||||
|
if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') {
|
||||||
|
expect(methodCall.arguments, isA<bool>());
|
||||||
|
lastFrameworkHandlesBack = methodCall.arguments as bool;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||||
|
SystemNavigator.setFrameworkHandlesBack(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('System back navigation inside of tabs', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
CupertinoApp(
|
||||||
|
home: MediaQuery(
|
||||||
|
data: const MediaQueryData(
|
||||||
|
viewInsets: EdgeInsets.only(bottom: 200),
|
||||||
|
),
|
||||||
|
child: CupertinoTabScaffold(
|
||||||
|
tabBar: _buildTabBar(),
|
||||||
|
tabBuilder: (BuildContext context, int index) {
|
||||||
|
return CupertinoTabView(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return CupertinoPageScaffold(
|
||||||
|
navigationBar: CupertinoNavigationBar(
|
||||||
|
middle: Text('Page 1 of tab ${index + 1}'),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: CupertinoButton(
|
||||||
|
child: const Text('Next page'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
CupertinoPageRoute<void>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return CupertinoPageScaffold(
|
||||||
|
navigationBar: CupertinoNavigationBar(
|
||||||
|
middle: Text('Page 2 of tab ${index + 1}'),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: CupertinoButton(
|
||||||
|
child: const Text('Back'),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsOneWidget);
|
||||||
|
expect(find.text('Page 2 of tab 1'), findsNothing);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next page'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsNothing);
|
||||||
|
expect(find.text('Page 2 of tab 1'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsOneWidget);
|
||||||
|
expect(find.text('Page 2 of tab 1'), findsNothing);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next page'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsNothing);
|
||||||
|
expect(find.text('Page 2 of tab 1'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Tab 2'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1 of tab 2'), findsOneWidget);
|
||||||
|
expect(find.text('Page 2 of tab 2'), findsNothing);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Tab 1'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsNothing);
|
||||||
|
expect(find.text('Page 2 of tab 1'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1 of tab 1'), findsOneWidget);
|
||||||
|
expect(find.text('Page 2 of tab 1'), findsNothing);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Tab 2'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Page 1 of tab 2'), findsOneWidget);
|
||||||
|
expect(find.text('Page 2 of tab 2'), findsNothing);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: kIsWeb, // [intended] frameworkHandlesBack not used on web.
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
|
CupertinoTabBar _buildTabBar({ int selectedTab = 0 }) {
|
||||||
|
@ -6,9 +6,12 @@ import 'dart:ui' show FlutterView;
|
|||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'navigator_utils.dart';
|
||||||
import 'observer_tester.dart';
|
import 'observer_tester.dart';
|
||||||
import 'semantics_tester.dart';
|
import 'semantics_tester.dart';
|
||||||
|
|
||||||
@ -4153,6 +4156,719 @@ void main() {
|
|||||||
expect(const RouteSettings().toString(), 'RouteSettings(none, null)');
|
expect(const RouteSettings().toString(), 'RouteSettings(none, null)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('Android Predictive Back', () {
|
||||||
|
bool? lastFrameworkHandlesBack;
|
||||||
|
setUp(() {
|
||||||
|
// Initialize to false. Because this uses a static boolean internally, it
|
||||||
|
// is not reset between tests or calls to pumpWidget. Explicitly setting
|
||||||
|
// it to false before each test makes them behave deterministically.
|
||||||
|
SystemNavigator.setFrameworkHandlesBack(false);
|
||||||
|
lastFrameworkHandlesBack = null;
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||||
|
if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') {
|
||||||
|
expect(methodCall.arguments, isA<bool>());
|
||||||
|
lastFrameworkHandlesBack = methodCall.arguments as bool;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||||
|
SystemNavigator.setFrameworkHandlesBack(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a single route is already defaulted to false', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
const MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: Text('home'),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('navigating around a single Navigator with .pop', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/one': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Page one',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one/one'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/one/one': (BuildContext context) => const _LinksPage(
|
||||||
|
title: 'Page one - one',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go back'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one/one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one - one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go back'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go back'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('navigating around a single Navigator with system back', (WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/one': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Page one',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one/one'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/one/one': (BuildContext context) => const _LinksPage(
|
||||||
|
title: 'Page one - one',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one/one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one - one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('a single Navigator with a PopScope that defaults to enabled', (WidgetTester tester) async {
|
||||||
|
bool canPop = true;
|
||||||
|
late StateSetter setState;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setter) {
|
||||||
|
setState = setter;
|
||||||
|
return MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
canPop: canPop,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = false;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = true;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('a single Navigator with a PopScope that defaults to disabled', (WidgetTester tester) async {
|
||||||
|
bool canPop = false;
|
||||||
|
late StateSetter setState;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setter) {
|
||||||
|
setState = setter;
|
||||||
|
return MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
canPop: canPop,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = true;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = false;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test both system back gestures and Navigator.pop.
|
||||||
|
for (final _BackType backType in _BackType.values) {
|
||||||
|
testWidgets('navigating around nested Navigators', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>();
|
||||||
|
final GlobalKey<NavigatorState> nestedNav = GlobalKey<NavigatorState>();
|
||||||
|
Future<void> goBack() async {
|
||||||
|
switch (backType) {
|
||||||
|
case _BackType.systemBack:
|
||||||
|
return simulateSystemBack();
|
||||||
|
case _BackType.navigatorPop:
|
||||||
|
if (nestedNav.currentState != null) {
|
||||||
|
if (nestedNav.currentState!.mounted && nestedNav.currentState!.canPop()) {
|
||||||
|
return nestedNav.currentState?.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nav.currentState?.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
navigatorKey: nav,
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/nested');
|
||||||
|
},
|
||||||
|
child: const Text('Go to nested'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/one': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Page one',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one/one'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/nested': (BuildContext context) => _NestedNavigatorsPage(
|
||||||
|
navigatorKey: nestedNav,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await goBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to nested'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - home'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to nested/one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await goBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - home'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await goBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('nested Navigators with a nested PopScope', (WidgetTester tester) async {
|
||||||
|
bool canPop = true;
|
||||||
|
late StateSetter setState;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setter) {
|
||||||
|
setState = setter;
|
||||||
|
return MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/nested');
|
||||||
|
},
|
||||||
|
child: const Text('Go to nested'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/one': (BuildContext context) => _LinksPage(
|
||||||
|
title: 'Page one',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to one/one'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
'/nested': (BuildContext context) => _NestedNavigatorsPage(
|
||||||
|
popScopePageEnabled: canPop,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to nested'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - home'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to nested/popscope'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - PopScope'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
// Going back works because canPop is true.
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - home'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to nested/popscope'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - PopScope'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = false;
|
||||||
|
});
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
// Now going back doesn't work because canPop is false, but it still
|
||||||
|
// has no effect on the system navigator due to all of the other routes.
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - PopScope'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = true;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
// And going back works again after switching canPop back to true.
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Nested - home'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
|
||||||
|
group('Navigator page API', () {
|
||||||
|
testWidgets('starting with one route as usual', (WidgetTester tester) async {
|
||||||
|
late StateSetter builderSetState;
|
||||||
|
final List<_Page> pages = <_Page>[_Page.home];
|
||||||
|
bool canPop() => pages.length <= 1;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
builderSetState = setState;
|
||||||
|
return PopScope(
|
||||||
|
canPop: canPop(),
|
||||||
|
onPopInvoked: (bool success) {
|
||||||
|
if (success || pages.last == _Page.noPop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
pages.removeLast();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Navigator(
|
||||||
|
onPopPage: (Route<void> route, void result) {
|
||||||
|
if (!route.didPop(null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
pages.removeLast();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
pages: pages.map((_Page page) {
|
||||||
|
switch (page) {
|
||||||
|
case _Page.home:
|
||||||
|
return MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
pages.add(_Page.one);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Go to _Page.one'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
pages.add(_Page.noPop);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Go to _Page.noPop'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case _Page.one:
|
||||||
|
return const MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
title: 'Page one',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case _Page.noPop:
|
||||||
|
return const MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
title: 'Cannot pop page',
|
||||||
|
canPop: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to _Page.one'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Go to _Page.noPop'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Cannot pop page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Cannot pop page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
// Circumvent "Cannot pop page" by directly modifying pages.
|
||||||
|
builderSetState(() {
|
||||||
|
pages.removeLast();
|
||||||
|
});
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('starting with existing route history', (WidgetTester tester) async {
|
||||||
|
final List<_Page> pages = <_Page>[_Page.home, _Page.one];
|
||||||
|
bool canPop() => pages.length <= 1;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter setState) {
|
||||||
|
return PopScope(
|
||||||
|
canPop: canPop(),
|
||||||
|
onPopInvoked: (bool success) {
|
||||||
|
if (success || pages.last == _Page.noPop) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
pages.removeLast();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Navigator(
|
||||||
|
onPopPage: (Route<void> route, void result) {
|
||||||
|
if (!route.didPop(null)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
pages.removeLast();
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
pages: pages.map((_Page page) {
|
||||||
|
switch (page) {
|
||||||
|
case _Page.home:
|
||||||
|
return MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
title: 'Home page',
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
pages.add(_Page.one);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Go to _Page.one'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
pages.add(_Page.noPop);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: const Text('Go to _Page.noPop'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case _Page.one:
|
||||||
|
return const MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
title: 'Page one',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
case _Page.noPop:
|
||||||
|
return const MaterialPage<void>(
|
||||||
|
child: _LinksPage(
|
||||||
|
title: 'Cannot pop page',
|
||||||
|
canPop: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsNothing);
|
||||||
|
expect(find.text('Page one'), findsOneWidget);
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Home page'), findsOneWidget);
|
||||||
|
expect(find.text('Page one'), findsNothing);
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
},
|
||||||
|
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.android }),
|
||||||
|
skip: isBrowser, // [intended] only non-web Android supports predictive back.
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef AnnouncementCallBack = void Function(Route<dynamic>?);
|
typedef AnnouncementCallBack = void Function(Route<dynamic>?);
|
||||||
@ -4435,3 +5151,153 @@ class TestDependencies extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum _BackType {
|
||||||
|
systemBack,
|
||||||
|
navigatorPop,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum _Page {
|
||||||
|
home,
|
||||||
|
one,
|
||||||
|
noPop,
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LinksPage extends StatelessWidget {
|
||||||
|
const _LinksPage ({
|
||||||
|
this.buttons = const <Widget>[],
|
||||||
|
this.canPop,
|
||||||
|
required this.title,
|
||||||
|
this.onBack,
|
||||||
|
});
|
||||||
|
|
||||||
|
final List<Widget> buttons;
|
||||||
|
final bool? canPop;
|
||||||
|
final VoidCallback? onBack;
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text(title),
|
||||||
|
...buttons,
|
||||||
|
if (Navigator.of(context).canPop())
|
||||||
|
TextButton(
|
||||||
|
onPressed: onBack ?? () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Go back'),
|
||||||
|
),
|
||||||
|
if (canPop != null)
|
||||||
|
PopScope(
|
||||||
|
canPop: canPop!,
|
||||||
|
child: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NestedNavigatorsPage extends StatefulWidget {
|
||||||
|
const _NestedNavigatorsPage({
|
||||||
|
this.popScopePageEnabled,
|
||||||
|
this.navigatorKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Whether the PopScope on the /popscope page is enabled.
|
||||||
|
///
|
||||||
|
/// If null, then no PopScope is built at all.
|
||||||
|
final bool? popScopePageEnabled;
|
||||||
|
|
||||||
|
final GlobalKey<NavigatorState>? navigatorKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_NestedNavigatorsPage> createState() => _NestedNavigatorsPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _NestedNavigatorsPageState extends State<_NestedNavigatorsPage> {
|
||||||
|
late final GlobalKey<NavigatorState> _navigatorKey;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_navigatorKey = widget.navigatorKey ?? GlobalKey<NavigatorState>();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final BuildContext rootContext = context;
|
||||||
|
return NavigatorPopHandler(
|
||||||
|
onPop: () {
|
||||||
|
if (widget.popScopePageEnabled == false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_navigatorKey.currentState!.pop();
|
||||||
|
},
|
||||||
|
child: Navigator(
|
||||||
|
key: _navigatorKey,
|
||||||
|
initialRoute: '/',
|
||||||
|
onGenerateRoute: (RouteSettings settings) {
|
||||||
|
switch (settings.name) {
|
||||||
|
case '/':
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return _LinksPage(
|
||||||
|
title: 'Nested - home',
|
||||||
|
onBack: () {
|
||||||
|
Navigator.of(rootContext).pop();
|
||||||
|
},
|
||||||
|
buttons: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one');
|
||||||
|
},
|
||||||
|
child: const Text('Go to nested/one'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/popscope');
|
||||||
|
},
|
||||||
|
child: const Text('Go to nested/popscope'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(rootContext).pop();
|
||||||
|
},
|
||||||
|
child: const Text('Go back out of nested nav'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case '/one':
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return const _LinksPage(
|
||||||
|
title: 'Nested - page one',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
case '/popscope':
|
||||||
|
return MaterialPageRoute<void>(
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return _LinksPage(
|
||||||
|
canPop: widget.popScopePageEnabled,
|
||||||
|
title: 'Nested - PopScope',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw Exception('Invalid route: ${settings.name}');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
20
packages/flutter/test/widgets/navigator_utils.dart
Normal file
20
packages/flutter/test/widgets/navigator_utils.dart
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
// 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/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
/// Simulates a system back, like a back gesture on Android.
|
||||||
|
///
|
||||||
|
/// Sends the same platform channel message that the engine sends when it
|
||||||
|
/// receives a system back.
|
||||||
|
Future<void> simulateSystemBack() {
|
||||||
|
return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage(
|
||||||
|
'flutter/navigation',
|
||||||
|
const JSONMessageCodec().encodeMessage(<String, dynamic>{
|
||||||
|
'method': 'popRoute',
|
||||||
|
}),
|
||||||
|
(ByteData? _) {},
|
||||||
|
);
|
||||||
|
}
|
361
packages/flutter/test/widgets/pop_scope_test.dart
Normal file
361
packages/flutter/test/widgets/pop_scope_test.dart
Normal file
@ -0,0 +1,361 @@
|
|||||||
|
// 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/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'navigator_utils.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
bool? lastFrameworkHandlesBack;
|
||||||
|
setUp(() {
|
||||||
|
// Initialize to false. Because this uses a static boolean internally, it
|
||||||
|
// is not reset between tests or calls to pumpWidget. Explicitly setting
|
||||||
|
// it to false before each test makes them behave deterministically.
|
||||||
|
SystemNavigator.setFrameworkHandlesBack(false);
|
||||||
|
lastFrameworkHandlesBack = null;
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
|
||||||
|
if (methodCall.method == 'SystemNavigator.setFrameworkHandlesBack') {
|
||||||
|
expect(methodCall.arguments, isA<bool>());
|
||||||
|
lastFrameworkHandlesBack = methodCall.arguments as bool;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
|
||||||
|
.setMockMethodCallHandler(SystemChannels.platform, null);
|
||||||
|
SystemNavigator.setFrameworkHandlesBack(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('toggling canPop on root route allows/prevents backs', (WidgetTester tester) async {
|
||||||
|
bool canPop = false;
|
||||||
|
late StateSetter setState;
|
||||||
|
late BuildContext context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext buildContext) => Scaffold(
|
||||||
|
body: StatefulBuilder(
|
||||||
|
builder: (BuildContext buildContext, StateSetter stateSetter) {
|
||||||
|
context = buildContext;
|
||||||
|
setState = stateSetter;
|
||||||
|
return PopScope(
|
||||||
|
canPop: canPop,
|
||||||
|
child: const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Home/PopScope Page'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = true;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
}
|
||||||
|
expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('toggling canPop on secondary route allows/prevents backs', (WidgetTester tester) async {
|
||||||
|
final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>();
|
||||||
|
bool canPop = true;
|
||||||
|
late StateSetter setState;
|
||||||
|
late BuildContext homeContext;
|
||||||
|
late BuildContext oneContext;
|
||||||
|
late bool lastPopSuccess;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
navigatorKey: nav,
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext context) {
|
||||||
|
homeContext = context;
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text('Home Page'),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pushNamed('/one');
|
||||||
|
},
|
||||||
|
child: const Text('Next'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
'/one': (BuildContext context) => Scaffold(
|
||||||
|
body: StatefulBuilder(
|
||||||
|
builder: (BuildContext context, StateSetter stateSetter) {
|
||||||
|
oneContext = context;
|
||||||
|
setState = stateSetter;
|
||||||
|
return PopScope(
|
||||||
|
canPop: canPop,
|
||||||
|
onPopInvoked: (bool didPop) {
|
||||||
|
lastPopSuccess = didPop;
|
||||||
|
},
|
||||||
|
child: const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('PopScope Page'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Home Page'), findsOneWidget);
|
||||||
|
expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('PopScope Page'), findsOneWidget);
|
||||||
|
expect(find.text('Home Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When canPop is true, can use pop to go back.
|
||||||
|
nav.currentState!.maybePop();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(lastPopSuccess, true);
|
||||||
|
expect(find.text('Home Page'), findsOneWidget);
|
||||||
|
expect(find.text('PopScope Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('PopScope Page'), findsOneWidget);
|
||||||
|
expect(find.text('Home Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// When canPop is true, can use system back to go back.
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(lastPopSuccess, true);
|
||||||
|
expect(find.text('Home Page'), findsOneWidget);
|
||||||
|
expect(find.text('PopScope Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('PopScope Page'), findsOneWidget);
|
||||||
|
expect(find.text('Home Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
canPop = false;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
// When canPop is false, can't use pop to go back.
|
||||||
|
nav.currentState!.maybePop();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(lastPopSuccess, false);
|
||||||
|
expect(find.text('PopScope Page'), findsOneWidget);
|
||||||
|
expect(find.text('Home Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.doNotPop);
|
||||||
|
|
||||||
|
// When canPop is false, can't use system back to go back.
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(lastPopSuccess, false);
|
||||||
|
expect(find.text('PopScope Page'), findsOneWidget);
|
||||||
|
expect(find.text('Home Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.doNotPop);
|
||||||
|
|
||||||
|
// Toggle canPop back to true and back works again.
|
||||||
|
setState(() {
|
||||||
|
canPop = true;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
nav.currentState!.maybePop();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(lastPopSuccess, true);
|
||||||
|
expect(find.text('Home Page'), findsOneWidget);
|
||||||
|
expect(find.text('PopScope Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.tap(find.text('Next'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('PopScope Page'), findsOneWidget);
|
||||||
|
expect(find.text('Home Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(oneContext)!.popDisposition, RoutePopDisposition.pop);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
}
|
||||||
|
|
||||||
|
await simulateSystemBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(lastPopSuccess, true);
|
||||||
|
expect(find.text('Home Page'), findsOneWidget);
|
||||||
|
expect(find.text('PopScope Page'), findsNothing);
|
||||||
|
expect(ModalRoute.of(homeContext)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('removing PopScope from the tree removes its effect on navigation', (WidgetTester tester) async {
|
||||||
|
bool usePopScope = true;
|
||||||
|
late StateSetter setState;
|
||||||
|
late BuildContext context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
initialRoute: '/',
|
||||||
|
routes: <String, WidgetBuilder>{
|
||||||
|
'/': (BuildContext buildContext) => Scaffold(
|
||||||
|
body: StatefulBuilder(
|
||||||
|
builder: (BuildContext buildContext, StateSetter stateSetter) {
|
||||||
|
context = buildContext;
|
||||||
|
setState = stateSetter;
|
||||||
|
const Widget child = Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Text('Home/PopScope Page'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!usePopScope) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
return const PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
}
|
||||||
|
expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
usePopScope = false;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
}
|
||||||
|
expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('identical PopScopes', (WidgetTester tester) async {
|
||||||
|
bool usePopScope1 = true;
|
||||||
|
bool usePopScope2 = true;
|
||||||
|
late StateSetter setState;
|
||||||
|
late BuildContext context;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: StatefulBuilder(
|
||||||
|
builder: (BuildContext buildContext, StateSetter stateSetter) {
|
||||||
|
context = buildContext;
|
||||||
|
setState = stateSetter;
|
||||||
|
return Column(
|
||||||
|
children: <Widget>[
|
||||||
|
if (usePopScope1)
|
||||||
|
const PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: Text('hello'),
|
||||||
|
),
|
||||||
|
if (usePopScope2)
|
||||||
|
const PopScope(
|
||||||
|
canPop: false,
|
||||||
|
child: Text('hello'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
}
|
||||||
|
expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop);
|
||||||
|
|
||||||
|
// Despite being in the widget tree twice, the ModalRoute has only ever
|
||||||
|
// registered one PopScopeInterface for it. Removing one makes it think that
|
||||||
|
// both have been removed.
|
||||||
|
setState(() {
|
||||||
|
usePopScope1 = false;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isTrue);
|
||||||
|
}
|
||||||
|
expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.doNotPop);
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
usePopScope2 = false;
|
||||||
|
});
|
||||||
|
await tester.pump();
|
||||||
|
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||||
|
expect(lastFrameworkHandlesBack, isFalse);
|
||||||
|
}
|
||||||
|
expect(ModalRoute.of(context)!.popDisposition, RoutePopDisposition.bubble);
|
||||||
|
},
|
||||||
|
variant: TargetPlatformVariant.all(),
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user