Cherry pick of revert (#132174)
CP: 2728ba0f23aeed05ae752cdd60e940dadf942ddf
diff --git a/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
index a68c2a1..f6626b8 100644
--- a/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
+++ b/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
@@ -48,9 +48,9 @@
@override
Widget build(BuildContext context) {
- return PopScope(
+ return WillPopScope(
// Prevent swipe popping of this page. Use explicit exit buttons only.
- canPop: false,
+ onWillPop: () => Future<bool>.value(true),
child: DefaultTextStyle(
style: CupertinoTheme.of(context).textTheme.textStyle,
child: CupertinoTabScaffold(
diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart
index 81ee4da..b6f7bbe 100644
--- a/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart
+++ b/dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart
@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:intl/intl.dart';
// This demo is based on
@@ -110,15 +109,16 @@
bool _hasName = false;
late String _eventName;
- Future<void> _handlePopInvoked(bool didPop) async {
- if (didPop) {
- return;
+ Future<bool> _onWillPop() async {
+ _saveNeeded = _hasLocation || _hasName || _saveNeeded;
+ if (!_saveNeeded) {
+ return true;
}
final ThemeData theme = Theme.of(context);
final TextStyle dialogTextStyle = theme.textTheme.titleMedium!.copyWith(color: theme.textTheme.bodySmall!.color);
- final bool? shouldDiscard = await showDialog<bool>(
+ return showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
@@ -130,31 +130,19 @@
TextButton(
child: const Text('CANCEL'),
onPressed: () {
- // Pop the confirmation dialog and indicate that the page should
- // not be popped.
- Navigator.of(context).pop(false);
+ Navigator.of(context).pop(false); // Pops the confirmation dialog but not the page.
},
),
TextButton(
child: const Text('DISCARD'),
onPressed: () {
- // Pop the confirmation dialog and indicate that the page should
- // be popped, too.
- Navigator.of(context).pop(true);
+ Navigator.of(context).pop(true); // Returning true to _onWillPop will pop again.
},
),
],
);
},
- );
-
- 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();
- }
+ ) as Future<bool>;
}
@override
@@ -174,8 +162,7 @@
],
),
body: Form(
- canPop: !_saveNeeded && !_hasLocation && !_hasName,
- onPopInvoked: _handlePopInvoked,
+ onWillPop: _onWillPop,
child: Scrollbar(
child: ListView(
primary: true,
diff --git a/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart
index c6f644e..5d3fee8 100644
--- a/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart
+++ b/dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart
@@ -143,9 +143,10 @@
return null;
}
- Future<void> _handlePopInvoked(bool didPop) async {
- if (didPop) {
- return;
+ Future<bool> _warnUserAboutInvalidData() async {
+ final FormState? form = _formKey.currentState;
+ if (form == null || !_formWasEdited || form.validate()) {
+ return true;
}
final bool? result = await showDialog<bool>(
@@ -167,14 +168,7 @@
);
},
);
-
- 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();
- }
+ return result!;
}
@override
@@ -191,8 +185,7 @@
child: Form(
key: _formKey,
autovalidateMode: _autovalidateMode,
- canPop: _formKey.currentState == null || !_formWasEdited || _formKey.currentState!.validate(),
- onPopInvoked: _handlePopInvoked,
+ onWillPop: _warnUserAboutInvalidData,
child: Scrollbar(
child: SingleChildScrollView(
primary: true,
diff --git a/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart b/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart
index de2cfa4..0391ef9 100644
--- a/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart
+++ b/dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart
@@ -3,6 +3,7 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:scoped_model/scoped_model.dart';
import 'colors.dart';
@@ -360,12 +361,14 @@
// Closes the cart if the cart is open, otherwise exits the app (this should
// only be relevant for Android).
- void _handlePopInvoked(bool didPop) {
- if (didPop) {
- return;
+ Future<bool> _onWillPop() async {
+ if (!_isOpen) {
+ await SystemNavigator.pop();
+ return true;
}
close();
+ return true;
}
@override
@@ -375,9 +378,8 @@
duration: const Duration(milliseconds: 225),
curve: Curves.easeInOut,
alignment: FractionalOffset.topLeft,
- child: PopScope(
- canPop: !_isOpen,
- onPopInvoked: _handlePopInvoked,
+ child: WillPopScope(
+ onWillPop: _onWillPop,
child: AnimatedBuilder(
animation: widget.hideController,
builder: _buildSlideAnimation,
diff --git a/dev/integration_tests/flutter_gallery/lib/gallery/home.dart b/dev/integration_tests/flutter_gallery/lib/gallery/home.dart
index 4a5f01f..c661ce6 100644
--- a/dev/integration_tests/flutter_gallery/lib/gallery/home.dart
+++ b/dev/integration_tests/flutter_gallery/lib/gallery/home.dart
@@ -325,14 +325,14 @@
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
body: SafeArea(
bottom: false,
- child: PopScope(
- canPop: _category == null,
- onPopInvoked: (bool didPop) {
- if (didPop) {
- return;
- }
+ child: WillPopScope(
+ onWillPop: () {
// Pop the category page if Android back button is pressed.
- setState(() => _category = null);
+ if (_category != null) {
+ setState(() => _category = null);
+ return Future<bool>.value(false);
+ }
+ return Future<bool>.value(true);
},
child: Backdrop(
backTitle: const Text('Options'),
diff --git a/examples/api/lib/material/navigation_bar/navigation_bar.2.dart b/examples/api/lib/material/navigation_bar/navigation_bar.2.dart
index 981eb5d..7af02cf 100644
--- a/examples/api/lib/material/navigation_bar/navigation_bar.2.dart
+++ b/examples/api/lib/material/navigation_bar/navigation_bar.2.dart
@@ -71,10 +71,14 @@
@override
Widget build(BuildContext context) {
- return NavigatorPopHandler(
- onPop: () {
+ return WillPopScope(
+ onWillPop: () async {
final NavigatorState navigator = navigatorKeys[selectedIndex].currentState!;
+ if (!navigator.canPop()) {
+ return true;
+ }
navigator.pop();
+ return false;
},
child: Scaffold(
body: SafeArea(
diff --git a/examples/api/lib/widgets/form/form.1.dart b/examples/api/lib/widgets/form/form.1.dart
deleted file mode 100644
index b5e8b6e..0000000
--- a/examples/api/lib/widgets/form/form.1.dart
+++ /dev/null
@@ -1,166 +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';
-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'),
- ),
- ],
- ),
- );
- }
-}
diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart
deleted file mode 100644
index d81b74f..0000000
--- a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart
+++ /dev/null
@@ -1,164 +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';
-
-/// 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'),
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart b/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart
deleted file mode 100644
index 04fbded..0000000
--- a/examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart
+++ /dev/null
@@ -1,250 +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.
-
-// 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,
- ],
- ),
- ),
- );
- }
-}
diff --git a/examples/api/lib/widgets/pop_scope/pop_scope.0.dart b/examples/api/lib/widgets/pop_scope/pop_scope.0.dart
deleted file mode 100644
index 6d144bd..0000000
--- a/examples/api/lib/widgets/pop_scope/pop_scope.0.dart
+++ /dev/null
@@ -1,128 +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.
-
-// 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'),
- ),
- ),
- ],
- ),
- ),
- );
- }
-}
diff --git a/examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart b/examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart
new file mode 100644
index 0000000..46dafa5
--- /dev/null
+++ b/examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart
@@ -0,0 +1,77 @@
+// 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'),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/examples/api/test/widgets/form/form.1_test.dart b/examples/api/test/widgets/form/form.1_test.dart
deleted file mode 100644
index f9ccd71..0000000
--- a/examples/api/test/widgets/form/form.1_test.dart
+++ /dev/null
@@ -1,37 +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';
-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);
- });
-}
diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart
deleted file mode 100644
index 88f2922..0000000
--- a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart
+++ /dev/null
@@ -1,48 +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/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);
- });
-}
diff --git a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart b/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart
deleted file mode 100644
index a6ea0ac..0000000
--- a/examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart
+++ /dev/null
@@ -1,38 +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/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);
- });
-}
diff --git a/examples/api/test/widgets/navigator_utils.dart b/examples/api/test/widgets/navigator_utils.dart
deleted file mode 100644
index 46f1f9b..0000000
--- a/examples/api/test/widgets/navigator_utils.dart
+++ /dev/null
@@ -1,20 +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/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? _) {},
- );
-}
diff --git a/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart b/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart
deleted file mode 100644
index ac334fc..0000000
--- a/examples/api/test/widgets/pop_scope/pop_scope.0_test.dart
+++ /dev/null
@@ -1,66 +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/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);
- });
-}
diff --git a/examples/api/test/widgets/will_pop_scope/will_pop_scope.0_test.dart b/examples/api/test/widgets/will_pop_scope/will_pop_scope.0_test.dart
new file mode 100644
index 0000000..ad05943
--- /dev/null
+++ b/examples/api/test/widgets/will_pop_scope/will_pop_scope.0_test.dart
@@ -0,0 +1,32 @@
+// 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));
+ });
+}
diff --git a/packages/flutter/lib/src/cupertino/app.dart b/packages/flutter/lib/src/cupertino/app.dart
index 6501b9c..47e45b1 100644
--- a/packages/flutter/lib/src/cupertino/app.dart
+++ b/packages/flutter/lib/src/cupertino/app.dart
@@ -157,7 +157,6 @@
this.onGenerateRoute,
this.onGenerateInitialRoutes,
this.onUnknownRoute,
- this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
this.builder,
this.title = '',
@@ -203,7 +202,6 @@
this.builder,
this.title = '',
this.onGenerateTitle,
- this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
this.color,
this.locale,
this.localizationsDelegates,
@@ -270,9 +268,6 @@
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
final RouteFactory? onUnknownRoute;
- /// {@macro flutter.widgets.widgetsApp.onNavigationNotification}
- final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
-
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver>? navigatorObservers;
@@ -578,7 +573,6 @@
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
- onNavigationNotification: widget.onNavigationNotification,
builder: widget.builder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart
index 2115782..f922eaf 100644
--- a/packages/flutter/lib/src/cupertino/route.dart
+++ b/packages/flutter/lib/src/cupertino/route.dart
@@ -196,8 +196,7 @@
}
// 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.
- if (route.hasScopedWillPopCallback
- || route.popDisposition == RoutePopDisposition.doNotPop) {
+ if (route.hasScopedWillPopCallback) {
return false;
}
// Fullscreen dialogs aren't dismissible by back swipe.
diff --git a/packages/flutter/lib/src/cupertino/tab_view.dart b/packages/flutter/lib/src/cupertino/tab_view.dart
index f41d0a4..8728196 100644
--- a/packages/flutter/lib/src/cupertino/tab_view.dart
+++ b/packages/flutter/lib/src/cupertino/tab_view.dart
@@ -162,39 +162,15 @@
..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
Widget build(BuildContext context) {
- final Widget child = Navigator(
- key: _navigatorKey,
+ return Navigator(
+ key: widget.navigatorKey,
onGenerateRoute: _onGenerateRoute,
onUnknownRoute: _onUnknownRoute,
observers: _navigatorObservers,
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) {
diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart
index 76462c8..44d4e7b 100644
--- a/packages/flutter/lib/src/material/about.dart
+++ b/packages/flutter/lib/src/material/about.dart
@@ -1179,10 +1179,9 @@
_builtLayout = _LayoutMode.nested;
final MaterialPageRoute<void> masterPageRoute = _masterPageRoute(context);
- return NavigatorPopHandler(
- onPop: () {
- _navigatorKey.currentState!.maybePop();
- },
+ return WillPopScope(
+ // Push pop check into nested navigator.
+ onWillPop: () async => !(await _navigatorKey.currentState!.maybePop()),
child: Navigator(
key: _navigatorKey,
initialRoute: 'initial',
@@ -1235,10 +1234,12 @@
MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
- return PopScope(
- onPopInvoked: (bool didPop) {
+ return WillPopScope(
+ onWillPop: () async {
// No need for setState() as rebuild happens on navigation pop.
focus = _Focus.master;
+ Navigator.of(context).pop();
+ return false;
},
child: BlockSemantics(child: widget.detailPageBuilder(context, arguments, null)),
);
diff --git a/packages/flutter/lib/src/material/app.dart b/packages/flutter/lib/src/material/app.dart
index 62f0385..2438c45 100644
--- a/packages/flutter/lib/src/material/app.dart
+++ b/packages/flutter/lib/src/material/app.dart
@@ -214,7 +214,6 @@
this.onGenerateRoute,
this.onGenerateInitialRoutes,
this.onUnknownRoute,
- this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
this.builder,
this.title = '',
@@ -268,7 +267,6 @@
this.builder,
this.title = '',
this.onGenerateTitle,
- this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification,
this.color,
this.theme,
this.darkTheme,
@@ -345,9 +343,6 @@
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
final RouteFactory? onUnknownRoute;
- /// {@macro flutter.widgets.widgetsApp.onNavigationNotification}
- final NotificationListenerCallback<NavigationNotification>? onNavigationNotification;
-
/// {@macro flutter.widgets.widgetsApp.navigatorObservers}
final List<NavigatorObserver>? navigatorObservers;
@@ -1024,7 +1019,6 @@
onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute,
- onNavigationNotification: widget.onNavigationNotification,
builder: _materialBuilder,
title: widget.title,
onGenerateTitle: widget.onGenerateTitle,
diff --git a/packages/flutter/lib/src/services/system_navigator.dart b/packages/flutter/lib/src/services/system_navigator.dart
index 1ea16f9..9edff64 100644
--- a/packages/flutter/lib/src/services/system_navigator.dart
+++ b/packages/flutter/lib/src/services/system_navigator.dart
@@ -2,44 +2,10 @@
// 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 'system_channels.dart';
/// Controls specific aspects of the system navigation stack.
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
/// it.
///
diff --git a/packages/flutter/lib/src/widgets/app.dart b/packages/flutter/lib/src/widgets/app.dart
index 5d402c6..af5d404 100644
--- a/packages/flutter/lib/src/widgets/app.dart
+++ b/packages/flutter/lib/src/widgets/app.dart
@@ -19,7 +19,6 @@
import 'localizations.dart';
import 'media_query.dart';
import 'navigator.dart';
-import 'notification_listener.dart';
import 'pages.dart';
import 'performance_overlay.dart';
import 'restoration.dart';
@@ -314,7 +313,6 @@
this.onGenerateRoute,
this.onGenerateInitialRoutes,
this.onUnknownRoute,
- this.onNavigationNotification = defaultOnNavigationNotification,
List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
this.initialRoute,
this.pageRouteBuilder,
@@ -422,7 +420,6 @@
this.builder,
this.title = '',
this.onGenerateTitle,
- this.onNavigationNotification = defaultOnNavigationNotification,
this.textStyle,
required this.color,
this.locale,
@@ -704,17 +701,6 @@
/// {@endtemplate}
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}
/// The name of the first route to show, if a [Navigator] is built.
///
@@ -1328,15 +1314,6 @@
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
State<WidgetsApp> createState() => _WidgetsAppState();
}
@@ -1771,25 +1748,30 @@
assert(_debugCheckLocalizations(appLocale));
- Widget child = Shortcuts(
- debugLabel: '<Default WidgetsApp Shortcuts>',
- shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
- // DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
- // fall through to the defaultShortcuts.
- child: DefaultTextEditingShortcuts(
- child: Actions(
- actions: widget.actions ?? <Type, Action<Intent>>{
- ...WidgetsApp.defaultActions,
- ScrollIntent: Action<ScrollIntent>.overridable(context: context, defaultAction: ScrollAction()),
- },
- child: FocusTraversalGroup(
- policy: ReadingOrderTraversalPolicy(),
- child: TapRegionSurface(
- child: ShortcutRegistrar(
- child: Localizations(
- locale: appLocale,
- delegates: _localizationsDelegates.toList(),
- child: title,
+ return RootRestorationScope(
+ restorationId: widget.restorationScopeId,
+ child: SharedAppData(
+ child: Shortcuts(
+ debugLabel: '<Default WidgetsApp Shortcuts>',
+ shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
+ // DefaultTextEditingShortcuts is nested inside Shortcuts so that it can
+ // fall through to the defaultShortcuts.
+ child: DefaultTextEditingShortcuts(
+ child: Actions(
+ actions: widget.actions ?? <Type, Action<Intent>>{
+ ...WidgetsApp.defaultActions,
+ ScrollIntent: Action<ScrollIntent>.overridable(context: context, defaultAction: ScrollAction()),
+ },
+ child: FocusTraversalGroup(
+ policy: ReadingOrderTraversalPolicy(),
+ child: TapRegionSurface(
+ child: ShortcutRegistrar(
+ child: Localizations(
+ locale: appLocale,
+ delegates: _localizationsDelegates.toList(),
+ child: title,
+ ),
+ ),
),
),
),
@@ -1797,19 +1779,5 @@
),
),
);
-
- if (widget.onNavigationNotification != null) {
- child = NotificationListener<NavigationNotification>(
- onNotification: widget.onNavigationNotification,
- child: child,
- );
- }
-
- return RootRestorationScope(
- restorationId: widget.restorationScopeId,
- child: SharedAppData(
- child: child,
- ),
- );
}
}
diff --git a/packages/flutter/lib/src/widgets/binding.dart b/packages/flutter/lib/src/widgets/binding.dart
index 13efd54..900d34f 100644
--- a/packages/flutter/lib/src/widgets/binding.dart
+++ b/packages/flutter/lib/src/widgets/binding.dart
@@ -54,8 +54,9 @@
/// ** See code in examples/api/lib/widgets/binding/widget_binding_observer.0.dart **
/// {@end-tool}
abstract mixin class WidgetsBindingObserver {
- /// Called when the system tells the app to pop the current route, such as
- /// after a system back button press or back gesture.
+ /// Called when the system tells the app to pop the current route.
+ /// For example, on Android, this is called when the user presses
+ /// the back button.
///
/// Observers are notified in registration order until one returns
/// true. If none return true, the application quits.
@@ -68,8 +69,6 @@
///
/// This method exposes the `popRoute` notification from
/// [SystemChannels.navigation].
- ///
- /// {@macro flutter.widgets.AndroidPredictiveBack}
Future<bool> didPopRoute() => Future<bool>.value(false);
/// Called when the host tells the application to push a new route onto the
@@ -721,27 +720,6 @@
///
/// This method exposes the `popRoute` notification from
/// [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
@visibleForTesting
Future<void> handlePopRoute() async {
diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart
index 05ad855..76e8521 100644
--- a/packages/flutter/lib/src/widgets/form.dart
+++ b/packages/flutter/lib/src/widgets/form.dart
@@ -10,10 +10,8 @@
import 'basic.dart';
import 'framework.dart';
import 'navigator.dart';
-import 'pop_scope.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
-import 'routes.dart';
import 'will_pop_scope.dart';
// Duration for delay before announcement in IOS so that the announcement won't be interrupted.
@@ -54,17 +52,10 @@
const Form({
super.key,
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.onChanged,
AutovalidateMode? autovalidateMode,
- }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled,
- assert((onPopInvoked == null && canPop == null) || onWillPop == null, 'onWillPop is deprecated; use canPop and/or onPopInvoked.');
+ }) : autovalidateMode = autovalidateMode ?? AutovalidateMode.disabled;
/// Returns the [FormState] of the closest [Form] widget which encloses the
/// given context, or null if none is found.
@@ -143,44 +134,8 @@
///
/// * [WillPopScope], another widget that provides a way to intercept the
/// back button.
- @Deprecated(
- 'Use canPop and/or onPopInvoked instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
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.
///
/// In addition to this callback being invoked, all the form fields themselves
@@ -245,18 +200,6 @@
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(
onWillPop: widget.onWillPop,
child: _FormScope(
diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart
index f366dfb..43156aa 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -20,7 +20,6 @@
import 'focus_traversal.dart';
import 'framework.dart';
import 'heroes.dart';
-import 'notification_listener.dart';
import 'overlay.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
@@ -68,10 +67,6 @@
///
/// Used by [Form.onWillPop], [ModalRoute.addScopedWillPopCallback],
/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope].
-@Deprecated(
- 'Use PopInvokedCallback instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
-)
typedef WillPopCallback = Future<bool> Function();
/// Signature for the [Navigator.onPopPage] callback.
@@ -94,21 +89,19 @@
enum RoutePopDisposition {
/// Pop the route.
///
- /// If [Route.willPop] or [Route.popDisposition] return [pop] then the back
- /// button will actually pop the current route.
+ /// If [Route.willPop] returns [pop] then the back button will actually pop
+ /// the current route.
pop,
/// Do not pop the route.
///
- /// If [Route.willPop] or [Route.popDisposition] return [doNotPop] then the
- /// back button will be ignored.
+ /// If [Route.willPop] returns [doNotPop] then the back button will be ignored.
doNotPop,
/// Delegate this to the next level of navigation.
///
- /// If [Route.willPop] or [Route.popDisposition] return [bubble] then the back
- /// button will be handled by the [SystemNavigator], which will usually close
- /// the application.
+ /// If [Route.willPop] returns [bubble] then the back button will be handled
+ /// by the [SystemNavigator], which will usually close the application.
bubble,
}
@@ -301,51 +294,10 @@
/// mechanism.
/// * [WillPopScope], another widget that provides a way to intercept the
/// back button.
- @Deprecated(
- 'Use popDisposition instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
Future<RoutePopDisposition> willPop() async {
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.
bool get willHandlePopInternally => false;
@@ -2463,9 +2415,6 @@
/// the initial route.
///
/// If there is no [Navigator] in scope, returns false.
- ///
- /// Does not consider anything that might externally prevent popping, such as
- /// [PopEntry].
/// {@endtemplate}
///
/// See also:
@@ -2477,22 +2426,21 @@
return navigator != null && navigator.canPop();
}
- /// Consults the current route's [Route.popDisposition] getter or
- /// [Route.willPop] method, and acts accordingly, potentially popping the
- /// route as a result; returns whether the pop request should be considered
- /// handled.
+ /// Consults the current route's [Route.willPop] method, and acts accordingly,
+ /// potentially popping the route as a result; returns whether the pop request
+ /// should be considered handled.
///
/// {@template flutter.widgets.navigator.maybePop}
- /// If the [RoutePopDisposition] is [RoutePopDisposition.pop], then the [pop]
+ /// If [Route.willPop] returns [RoutePopDisposition.pop], then the [pop]
/// method is called, and this method returns true, indicating that it handled
/// the pop request.
///
- /// If the [RoutePopDisposition] is [RoutePopDisposition.doNotPop], then this
+ /// If [Route.willPop] returns [RoutePopDisposition.doNotPop], then this
/// method returns true, but does not do anything beyond that.
///
- /// If the [RoutePopDisposition] is [RoutePopDisposition.bubble], then this
- /// method returns false, and the caller is responsible for sending the
- /// request to the containing scope (e.g. by closing the application).
+ /// If [Route.willPop] returns [RoutePopDisposition.bubble], then this method
+ /// returns false, and the caller is responsible for sending the request to
+ /// the containing scope (e.g. by closing the application).
///
/// 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.
@@ -3067,7 +3015,6 @@
assert(isPresent);
pendingResult = result;
currentState = _RouteLifecycle.pop;
- route.onPopInvoked(true);
}
bool _reportRemovalToObserver = true;
@@ -3348,93 +3295,12 @@
}
}
-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.
///
/// A reference to this class can be obtained by calling [Navigator.of].
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
late GlobalKey<OverlayState> _overlayKey;
- final _History _history = _History();
-
+ List<_RouteEntry> _history = <_RouteEntry>[];
/// A set for entries that are waiting to dispose until their subtrees are
/// disposed.
///
@@ -3464,43 +3330,12 @@
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
void initState() {
super.initState();
assert(() {
- if (_usingPagesAPI) {
+ if (widget.pages != const <Page<dynamic>>[]) {
+ // This navigator uses page API.
if (widget.pages.isEmpty) {
FlutterError.reportError(
FlutterErrorDetails(
@@ -3543,8 +3378,6 @@
if (widget.reportsRouteUpdateToEngine) {
SystemNavigator.selectSingleEntryHistory();
}
-
- _history.addListener(_handleHistoryChanged);
}
// Use [_nextPagelessRestorationScopeId] to get the next id.
@@ -3727,7 +3560,7 @@
void didUpdateWidget(Navigator oldWidget) {
super.didUpdateWidget(oldWidget);
assert(() {
- if (_usingPagesAPI) {
+ if (widget.pages != const <Page<dynamic>>[]) {
// This navigator uses page API.
if (widget.pages.isEmpty) {
FlutterError.reportError(
@@ -3839,8 +3672,6 @@
_rawNextPagelessRestorationScopeId.dispose();
_serializableHistory.dispose();
userGestureInProgressNotifier.dispose();
- _history.removeListener(_handleHistoryChanged);
- _history.dispose();
super.dispose();
// don't unlock, so that the object becomes unusable
assert(_debugLocked);
@@ -4126,7 +3957,7 @@
pageRouteToPagelessRoutes: pageRouteToPagelessRoutes,
).cast<_RouteEntry>();
}
- _history.clear();
+ _history = <_RouteEntry>[];
// Adds the leading pageless routes if there is any.
if (pageRouteToPagelessRoutes.containsKey(null)) {
_history.addAll(pageRouteToPagelessRoutes[null]!);
@@ -5142,17 +4973,17 @@
return true; // there's at least two routes, so we can pop
}
- /// Consults the current route's [Route.popDisposition] method, and acts
- /// accordingly, potentially popping the route as a result; returns whether
- /// the pop request should be considered handled.
+ /// Consults the current route's [Route.willPop] method, and acts accordingly,
+ /// potentially popping the route as a result; returns whether the pop request
+ /// should be considered handled.
///
/// {@macro flutter.widgets.navigator.maybePop}
///
/// See also:
///
- /// * [Form], which provides a [Form.canPop] boolean that enables the
- /// form to prevent any [pop]s initiated by the app's back button.
- /// * [ModalRoute], which provides a `scopedOnPopCallback` that can be used
+ /// * [Form], which provides an `onWillPop` callback that enables the form
+ /// to veto a [pop] initiated by the app's back button.
+ /// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used
/// to define the route's `willPop` method.
@optionalTypeArgs
Future<bool> maybePop<T extends Object?>([ T? result ]) async {
@@ -5161,31 +4992,23 @@
return false;
}
assert(lastEntry.route._navigator == this);
-
- // TODO(justinmc): When the deprecated willPop method is removed, delete
- // this code and use only popDisposition, below.
- final RoutePopDisposition willPopDisposition = await lastEntry.route.willPop();
+ final RoutePopDisposition disposition = await lastEntry.route.willPop(); // this is asynchronous
if (!mounted) {
// Forget about this pop, we were disposed in the meantime.
return true;
}
- if (willPopDisposition == RoutePopDisposition.doNotPop) {
- return true;
- }
final _RouteEntry? newLastEntry = _lastRouteEntryWhereOrNull(_RouteEntry.isPresentPredicate);
if (lastEntry != newLastEntry) {
// Forget about this pop, something happened to our history in the meantime.
return true;
}
-
- switch (lastEntry.route.popDisposition) {
+ switch (disposition) {
case RoutePopDisposition.bubble:
return false;
case RoutePopDisposition.pop:
pop(result);
return true;
case RoutePopDisposition.doNotPop:
- lastEntry.route.onPopInvoked(false);
return true;
}
}
@@ -5475,46 +5298,29 @@
Widget build(BuildContext context) {
assert(!_debugLocked);
assert(_history.isNotEmpty);
-
// Hides the HeroControllerScope for the widget subtree so that the other
// nested navigator underneath will not pick up the hero controller above
// this level.
return HeroControllerScope.none(
- child: NotificationListener<NavigationNotification>(
- onNotification: (NavigationNotification notification) {
- // If the state of this Navigator does not change whether or not the
- // whole framework can pop, propagate the Notification as-is.
- if (notification.canHandlePop || !canPop()) {
- return false;
- }
- // Otherwise, dispatch a new Notification with the correct canPop and
- // stop the propagation of the old Notification.
- const NavigationNotification nextNotification = NavigationNotification(
- canHandlePop: true,
- );
- nextNotification.dispatch(context);
- return true;
- },
- child: Listener(
- onPointerDown: _handlePointerDown,
- onPointerUp: _handlePointerUpOrCancel,
- 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>[],
- ),
+ child: Listener(
+ onPointerDown: _handlePointerDown,
+ onPointerUp: _handlePointerUpOrCancel,
+ 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>[],
),
),
),
@@ -5675,7 +5481,7 @@
// Updating.
- void update(_History history) {
+ void update(List<_RouteEntry> history) {
assert(isRegistered);
final bool wasUninitialized = _pageToPagelessRoutes == null;
bool needsSerialization = wasUninitialized;
@@ -5998,26 +5804,3 @@
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';
- }
-}
diff --git a/packages/flutter/lib/src/widgets/navigator_pop_handler.dart b/packages/flutter/lib/src/widgets/navigator_pop_handler.dart
deleted file mode 100644
index 203a85b..0000000
--- a/packages/flutter/lib/src/widgets/navigator_pop_handler.dart
+++ /dev/null
@@ -1,110 +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 '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,
- ),
- );
- }
-}
diff --git a/packages/flutter/lib/src/widgets/pop_scope.dart b/packages/flutter/lib/src/widgets/pop_scope.dart
deleted file mode 100644
index b47d83f..0000000
--- a/packages/flutter/lib/src/widgets/pop_scope.dart
+++ /dev/null
@@ -1,137 +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/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;
-}
diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart
index e54e46a..441486e 100644
--- a/packages/flutter/lib/src/widgets/routes.dart
+++ b/packages/flutter/lib/src/widgets/routes.dart
@@ -717,10 +717,6 @@
}
}
- @Deprecated(
- 'Use popDisposition instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
@override
Future<RoutePopDisposition> willPop() async {
if (willHandlePopInternally) {
@@ -730,14 +726,6 @@
}
@override
- RoutePopDisposition get popDisposition {
- if (willHandlePopInternally) {
- return RoutePopDisposition.pop;
- }
- return super.popDisposition;
- }
-
- @override
bool didPop(T? result) {
if (_localHistory != null && _localHistory!.isNotEmpty) {
final LocalHistoryEntry entry = _localHistory!.removeLast();
@@ -1502,8 +1490,6 @@
final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
- final Set<PopEntry> _popEntries = <PopEntry>{};
-
/// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
/// [addScopedWillPopCallback] returns either false or null. If they all
/// return true, the base [Route.willPop]'s result will be returned. The
@@ -1522,10 +1508,6 @@
/// method checks.
/// * [removeScopedWillPopCallback], which removes a callback from the list
/// this method checks.
- @Deprecated(
- 'Use popDisposition instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
@override
Future<RoutePopDisposition> willPop() async {
final _ModalScopeState<T>? scope = _scopeKey.currentState;
@@ -1538,44 +1520,26 @@
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.
///
+ /// {@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
/// after its route has been disposed. The callback should check [State.mounted]
/// before doing anything.
@@ -1584,6 +1548,49 @@
/// 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.
///
+ /// {@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:
///
/// * [WillPopScope], which manages the registration and unregistration
@@ -1592,10 +1599,6 @@
/// * [willPop], which runs the callbacks added with this method.
/// * [removeScopedWillPopCallback], which removes a callback from the list
/// that [willPop] checks.
- @Deprecated(
- 'Use registerPopEntry or PopScope instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
void addScopedWillPopCallback(WillPopCallback callback) {
assert(_scopeKey.currentState != null, 'Tried to add a willPop callback to a route that is not currently in the tree.');
_willPopCallbacks.add(callback);
@@ -1608,69 +1611,11 @@
/// * [Form], which provides an `onWillPop` callback that uses this mechanism.
/// * [addScopedWillPopCallback], which adds callback to the list
/// checked by [willPop].
- @Deprecated(
- 'Use unregisterPopEntry or PopScope instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
void removeScopedWillPopCallback(WillPopCallback callback) {
assert(_scopeKey.currentState != null, 'Tried to remove a willPop callback from a route that is not currently in the tree.');
_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.
///
/// This method is used to disable the horizontal swipe pop gesture supported
@@ -1688,10 +1633,6 @@
/// * [removeScopedWillPopCallback], which removes a callback.
/// * [willHandlePopInternally], which reports on another reason why
/// a pop might be vetoed.
- @Deprecated(
- 'Use popDisposition instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
@protected
bool get hasScopedWillPopCallback {
return _willPopCallbacks.isNotEmpty;
@@ -1832,14 +1773,6 @@
}
@override
- bool get willHandlePopInternally {
- final bool popEntriesCanPop = _popEntries.every((PopEntry popEntry) {
- return popEntry.canPopNotifier.value;
- });
- return !popEntriesCanPop || super.willHandlePopInternally;
- }
-
- @override
String toString() => '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)';
}
@@ -2279,33 +2212,3 @@
///
/// See [ModalRoute.buildTransitions] for complete definition of the parameters.
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';
- }
-}
diff --git a/packages/flutter/lib/src/widgets/will_pop_scope.dart b/packages/flutter/lib/src/widgets/will_pop_scope.dart
index eefe437..ab90c7f 100644
--- a/packages/flutter/lib/src/widgets/will_pop_scope.dart
+++ b/packages/flutter/lib/src/widgets/will_pop_scope.dart
@@ -9,25 +9,26 @@
/// Registers a callback to veto attempts by the user to dismiss the enclosing
/// [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:
///
/// * [ModalRoute.addScopedWillPopCallback] and [ModalRoute.removeScopedWillPopCallback],
/// which this widget uses to register and unregister [onWillPop].
/// * [Form], which provides an `onWillPop` callback that enables the form
/// 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 {
/// Creates a widget that registers a callback to veto attempts by the user to
/// dismiss the enclosing [ModalRoute].
///
/// The [child] argument must not be null.
- @Deprecated(
- 'Use PopScope instead. '
- 'This feature was deprecated after v3.12.0-1.0.pre.',
- )
const WillPopScope({
super.key,
required this.child,
diff --git a/packages/flutter/lib/widgets.dart b/packages/flutter/lib/widgets.dart
index 3ca0999..539d6aa 100644
--- a/packages/flutter/lib/widgets.dart
+++ b/packages/flutter/lib/widgets.dart
@@ -81,7 +81,6 @@
export 'src/widgets/modal_barrier.dart';
export 'src/widgets/navigation_toolbar.dart';
export 'src/widgets/navigator.dart';
-export 'src/widgets/navigator_pop_handler.dart';
export 'src/widgets/nested_scroll_view.dart';
export 'src/widgets/notification_listener.dart';
export 'src/widgets/orientation_builder.dart';
@@ -96,7 +95,6 @@
export 'src/widgets/platform_menu_bar.dart';
export 'src/widgets/platform_selectable_region_context_menu.dart';
export 'src/widgets/platform_view.dart';
-export 'src/widgets/pop_scope.dart';
export 'src/widgets/preferred_size.dart';
export 'src/widgets/primary_scroll_controller.dart';
export 'src/widgets/raw_keyboard_listener.dart';
diff --git a/packages/flutter/test/cupertino/tab_scaffold_test.dart b/packages/flutter/test/cupertino/tab_scaffold_test.dart
index b376a92..a8a43b5 100644
--- a/packages/flutter/test/cupertino/tab_scaffold_test.dart
+++ b/packages/flutter/test/cupertino/tab_scaffold_test.dart
@@ -2,14 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:typed_data';
+
import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../image_data.dart';
import '../rendering/rendering_tester.dart' show TestCallbackPainter;
-import '../widgets/navigator_utils.dart';
late List<int> selectedTabs;
@@ -1216,132 +1215,6 @@
expect(find.text('Content 2'), 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 }) {
diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart
index d403ceb..26d1463 100644
--- a/packages/flutter/test/widgets/navigator_test.dart
+++ b/packages/flutter/test/widgets/navigator_test.dart
@@ -6,12 +6,9 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
-import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
-import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
-import 'navigator_utils.dart';
import 'observer_tester.dart';
import 'semantics_tester.dart';
@@ -4156,719 +4153,6 @@
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>?);
@@ -5151,153 +4435,3 @@
);
}
}
-
-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}');
- }
- },
- ),
- );
- }
-}
diff --git a/packages/flutter/test/widgets/navigator_utils.dart b/packages/flutter/test/widgets/navigator_utils.dart
deleted file mode 100644
index 46f1f9b..0000000
--- a/packages/flutter/test/widgets/navigator_utils.dart
+++ /dev/null
@@ -1,20 +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/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? _) {},
- );
-}
diff --git a/packages/flutter/test/widgets/pop_scope_test.dart b/packages/flutter/test/widgets/pop_scope_test.dart
deleted file mode 100644
index c5d0e88..0000000
--- a/packages/flutter/test/widgets/pop_scope_test.dart
+++ /dev/null
@@ -1,361 +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/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(),
- );
-}