From 2728ba0f23aeed05ae752cdd60e940dadf942ddf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jes=C3=BAs=20S=20Guerrero?= Date: Tue, 8 Aug 2023 16:16:52 -0700 Subject: [PATCH] Revert of #120385 (#132167) Breaking google testing revert of: https://github.com/flutter/flutter/pull/120385 b/295065534 --- .../cupertino/cupertino_navigation_demo.dart | 4 +- .../material/full_screen_dialog_demo.dart | 31 +- .../demo/material/text_form_field_demo.dart | 19 +- .../demo/shrine/expanding_bottom_sheet.dart | 14 +- .../flutter_gallery/lib/gallery/home.dart | 14 +- .../navigation_bar/navigation_bar.2.dart | 8 +- examples/api/lib/widgets/form/form.1.dart | 166 ---- .../navigator_pop_handler.0.dart | 164 ---- .../navigator_pop_handler.1.dart | 250 ----- .../lib/widgets/pop_scope/pop_scope.0.dart | 128 --- .../will_pop_scope/will_pop_scope.0.dart | 77 ++ .../api/test/widgets/form/form.1_test.dart | 37 - .../navigator_pop_handler.0_test.dart | 48 - .../navigator_pop_handler.1_test.dart | 38 - .../api/test/widgets/navigator_utils.dart | 20 - .../widgets/pop_scope/pop_scope.0_test.dart | 66 -- .../will_pop_scope/will_pop_scope.0_test.dart | 32 + packages/flutter/lib/src/cupertino/app.dart | 6 - packages/flutter/lib/src/cupertino/route.dart | 3 +- .../flutter/lib/src/cupertino/tab_view.dart | 28 +- packages/flutter/lib/src/material/about.dart | 13 +- packages/flutter/lib/src/material/app.dart | 6 - .../lib/src/services/system_navigator.dart | 34 - packages/flutter/lib/src/widgets/app.dart | 80 +- packages/flutter/lib/src/widgets/binding.dart | 28 +- packages/flutter/lib/src/widgets/form.dart | 59 +- .../flutter/lib/src/widgets/navigator.dart | 294 +----- .../src/widgets/navigator_pop_handler.dart | 110 --- .../flutter/lib/src/widgets/pop_scope.dart | 137 --- packages/flutter/lib/src/widgets/routes.dart | 219 ++--- .../lib/src/widgets/will_pop_scope.dart | 17 +- packages/flutter/lib/widgets.dart | 2 - .../test/cupertino/tab_scaffold_test.dart | 131 +-- .../flutter/test/widgets/navigator_test.dart | 866 ------------------ .../flutter/test/widgets/navigator_utils.dart | 20 - .../flutter/test/widgets/pop_scope_test.dart | 361 -------- 36 files changed, 303 insertions(+), 3227 deletions(-) delete mode 100644 examples/api/lib/widgets/form/form.1.dart delete mode 100644 examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.0.dart delete mode 100644 examples/api/lib/widgets/navigator_pop_handler/navigator_pop_handler.1.dart delete mode 100644 examples/api/lib/widgets/pop_scope/pop_scope.0.dart create mode 100644 examples/api/lib/widgets/will_pop_scope/will_pop_scope.0.dart delete mode 100644 examples/api/test/widgets/form/form.1_test.dart delete mode 100644 examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.0_test.dart delete mode 100644 examples/api/test/widgets/navigator_pop_handler/navigator_pop_handler.1_test.dart delete mode 100644 examples/api/test/widgets/navigator_utils.dart delete mode 100644 examples/api/test/widgets/pop_scope/pop_scope.0_test.dart create mode 100644 examples/api/test/widgets/will_pop_scope/will_pop_scope.0_test.dart delete mode 100644 packages/flutter/lib/src/widgets/navigator_pop_handler.dart delete mode 100644 packages/flutter/lib/src/widgets/pop_scope.dart delete mode 100644 packages/flutter/test/widgets/navigator_utils.dart delete mode 100644 packages/flutter/test/widgets/pop_scope_test.dart 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 a68c2a194c..f6626b85e2 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 @@ class CupertinoNavigationDemo extends StatelessWidget { @override Widget build(BuildContext context) { - return PopScope( + return WillPopScope( // Prevent swipe popping of this page. Use explicit exit buttons only. - canPop: false, + onWillPop: () => Future.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 81ee4dacd8..b6f7bbe6b6 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 @@ class FullScreenDialogDemoState extends State { bool _hasName = false; late String _eventName; - Future _handlePopInvoked(bool didPop) async { - if (didPop) { - return; + Future _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( + return showDialog( context: context, builder: (BuildContext context) { return AlertDialog( @@ -130,31 +130,19 @@ class FullScreenDialogDemoState extends State { 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; } @override @@ -174,8 +162,7 @@ class FullScreenDialogDemoState extends State { ], ), 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 c6f644ee74..5d3fee8d60 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 @@ class TextFormFieldDemoState extends State { return null; } - Future _handlePopInvoked(bool didPop) async { - if (didPop) { - return; + Future _warnUserAboutInvalidData() async { + final FormState? form = _formKey.currentState; + if (form == null || !_formWasEdited || form.validate()) { + return true; } final bool? result = await showDialog( @@ -167,14 +168,7 @@ class TextFormFieldDemoState extends State { ); }, ); - - 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 @@ class TextFormFieldDemoState extends State { 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 de2cfa43ef..0391ef95be 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 @@ class ExpandingBottomSheetState extends State with TickerP // 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 _onWillPop() async { + if (!_isOpen) { + await SystemNavigator.pop(); + return true; } close(); + return true; } @override @@ -375,9 +378,8 @@ class ExpandingBottomSheetState extends State with TickerP 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 4a5f01fede..c661ce6448 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 @@ class _GalleryHomeState extends State with SingleTickerProviderStat 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.value(false); + } + return Future.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 981eb5d812..7af02cfe00 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 @@ class _HomeState extends State with TickerProviderStateMixin { @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 b5e8b6e09c..0000000000 --- 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 _showDialog() async { - final bool? shouldDiscard = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Are you sure?'), - content: const Text('Any unsaved changes will be lost!'), - actions: [ - 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: [ - 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: [ - TextFormField( - controller: _controller, - onFieldSubmitted: (String? value) { - _save(value); - }, - ), - TextButton( - onPressed: () { - _save(_controller.text); - }, - child: Row( - children: [ - 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 d81b74f65f..0000000000 --- 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: { - '/': (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: [ - 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 createState() => _NestedNavigatorsPageState(); -} - -class _NestedNavigatorsPageState extends State { - final GlobalKey _nestedNavigatorKey = GlobalKey(); - - @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( - builder: (BuildContext context) => NestedNavigatorsPageOne( - onBack: () { - Navigator.of(rootContext).pop(); - }, - ), - ); - case 'nested_navigators/one/another_one': - return MaterialPageRoute( - 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: [ - 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: [ - 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 04fbdedd34..0000000000 --- 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: { - '/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 _navigatorKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return NavigatorPopHandler( - onPop: () { - _navigatorKey.currentState?.maybePop(); - }, - child: Navigator( - key: _navigatorKey, - onPopPage: (Route 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( - child: _LinksPage( - title: 'Bottom nav - tab ${widget.title} - route $page', - backgroundColor: widget.color, - buttons: [ - 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( - child: _LinksPage( - backgroundColor: widget.color, - title: 'Bottom nav - tab ${widget.title} - route $page', - buttons: [ - 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 [], - required this.title, - }); - - final Color backgroundColor; - final List buttons; - final String title; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: backgroundColor, - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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 6d144bd088..0000000000 --- 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: { - '/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: [ - 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( - 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: [ - 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: [ - 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 0000000000..46dafa57b9 --- /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 createState() => _WillPopScopeExampleState(); +} + +class _WillPopScopeExampleState extends State { + 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: [ + OutlinedButton( + child: const Text('Push'), + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute( + 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 f9ccd71d52..0000000000 --- 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 88f29223f6..0000000000 --- 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 a6ea0ac828..0000000000 --- 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 46f1f9b1ac..0000000000 --- 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 simulateSystemBack() { - return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/navigation', - const JSONMessageCodec().encodeMessage({ - '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 ac334fc322..0000000000 --- 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 0000000000..ad05943108 --- /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 6501b9cee3..47e45b198b 100644 --- a/packages/flutter/lib/src/cupertino/app.dart +++ b/packages/flutter/lib/src/cupertino/app.dart @@ -157,7 +157,6 @@ class CupertinoApp extends StatefulWidget { this.onGenerateRoute, this.onGenerateInitialRoutes, this.onUnknownRoute, - this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification, List this.navigatorObservers = const [], this.builder, this.title = '', @@ -203,7 +202,6 @@ class CupertinoApp extends StatefulWidget { this.builder, this.title = '', this.onGenerateTitle, - this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification, this.color, this.locale, this.localizationsDelegates, @@ -270,9 +268,6 @@ class CupertinoApp extends StatefulWidget { /// {@macro flutter.widgets.widgetsApp.onUnknownRoute} final RouteFactory? onUnknownRoute; - /// {@macro flutter.widgets.widgetsApp.onNavigationNotification} - final NotificationListenerCallback? onNavigationNotification; - /// {@macro flutter.widgets.widgetsApp.navigatorObservers} final List? navigatorObservers; @@ -578,7 +573,6 @@ class _CupertinoAppState extends State { 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 2115782148..f922eaf33e 100644 --- a/packages/flutter/lib/src/cupertino/route.dart +++ b/packages/flutter/lib/src/cupertino/route.dart @@ -196,8 +196,7 @@ mixin CupertinoRouteTransitionMixin on PageRoute { } // 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 f41d0a4a31..8728196eee 100644 --- a/packages/flutter/lib/src/cupertino/tab_view.dart +++ b/packages/flutter/lib/src/cupertino/tab_view.dart @@ -162,39 +162,15 @@ class _CupertinoTabViewState extends State { ..add(_heroController); } - GlobalKey? _ownedNavigatorKey; - GlobalKey get _navigatorKey { - if (widget.navigatorKey != null) { - return widget.navigatorKey!; - } - _ownedNavigatorKey ??= GlobalKey(); - 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? _onGenerateRoute(RouteSettings settings) { diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index 76462c8c60..44d4e7b1cf 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -1179,10 +1179,9 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp _builtLayout = _LayoutMode.nested; final MaterialPageRoute 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 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp MaterialPageRoute _detailPageRoute(Object? arguments) { return MaterialPageRoute(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 62f0385935..2438c45828 100644 --- a/packages/flutter/lib/src/material/app.dart +++ b/packages/flutter/lib/src/material/app.dart @@ -214,7 +214,6 @@ class MaterialApp extends StatefulWidget { this.onGenerateRoute, this.onGenerateInitialRoutes, this.onUnknownRoute, - this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification, List this.navigatorObservers = const [], this.builder, this.title = '', @@ -268,7 +267,6 @@ class MaterialApp extends StatefulWidget { this.builder, this.title = '', this.onGenerateTitle, - this.onNavigationNotification = WidgetsApp.defaultOnNavigationNotification, this.color, this.theme, this.darkTheme, @@ -345,9 +343,6 @@ class MaterialApp extends StatefulWidget { /// {@macro flutter.widgets.widgetsApp.onUnknownRoute} final RouteFactory? onUnknownRoute; - /// {@macro flutter.widgets.widgetsApp.onNavigationNotification} - final NotificationListenerCallback? onNavigationNotification; - /// {@macro flutter.widgets.widgetsApp.navigatorObservers} final List? navigatorObservers; @@ -1024,7 +1019,6 @@ class _MaterialAppState extends State { 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 1ea16f921a..9edff64b3c 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 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( - '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 5d402c67bc..af5d404c43 100644 --- a/packages/flutter/lib/src/widgets/app.dart +++ b/packages/flutter/lib/src/widgets/app.dart @@ -19,7 +19,6 @@ import 'framework.dart'; 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 @@ class WidgetsApp extends StatefulWidget { this.onGenerateRoute, this.onGenerateInitialRoutes, this.onUnknownRoute, - this.onNavigationNotification = defaultOnNavigationNotification, List this.navigatorObservers = const [], this.initialRoute, this.pageRouteBuilder, @@ -422,7 +420,6 @@ class WidgetsApp extends StatefulWidget { this.builder, this.title = '', this.onGenerateTitle, - this.onNavigationNotification = defaultOnNavigationNotification, this.textStyle, required this.color, this.locale, @@ -704,17 +701,6 @@ class WidgetsApp extends StatefulWidget { /// {@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? onNavigationNotification; - /// {@template flutter.widgets.widgetsApp.initialRoute} /// The name of the first route to show, if a [Navigator] is built. /// @@ -1328,15 +1314,6 @@ class WidgetsApp extends StatefulWidget { 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 createState() => _WidgetsAppState(); } @@ -1771,25 +1748,30 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { assert(_debugCheckLocalizations(appLocale)); - Widget child = Shortcuts( - debugLabel: '', - 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 ?? >{ - ...WidgetsApp.defaultActions, - ScrollIntent: Action.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: '', + 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 ?? >{ + ...WidgetsApp.defaultActions, + ScrollIntent: Action.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 @@ class _WidgetsAppState extends State with WidgetsBindingObserver { ), ), ); - - if (widget.onNavigationNotification != null) { - child = NotificationListener( - 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 13efd54f53..900d34fe77 100644 --- a/packages/flutter/lib/src/widgets/binding.dart +++ b/packages/flutter/lib/src/widgets/binding.dart @@ -54,8 +54,9 @@ export 'dart:ui' show AppLifecycleState, Locale; /// ** 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 @@ abstract mixin class WidgetsBindingObserver { /// /// This method exposes the `popRoute` notification from /// [SystemChannels.navigation]. - /// - /// {@macro flutter.widgets.AndroidPredictiveBack} Future didPopRoute() => Future.value(false); /// Called when the host tells the application to push a new route onto the @@ -721,27 +720,6 @@ mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureB /// /// 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 handlePopRoute() async { diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 05ad8554a6..76e8521ab9 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -10,10 +10,8 @@ import 'package:flutter/rendering.dart'; 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 @@ class Form extends StatefulWidget { 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 @@ class Form extends StatefulWidget { /// /// * [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 @@ class FormState extends State
{ 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 9a5485368b..43156aa44e 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -20,7 +20,6 @@ import 'focus_scope.dart'; 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 @@ typedef RoutePredicate = bool Function(Route route); /// /// 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 Function(); /// Signature for the [Navigator.onPopPage] callback. @@ -94,21 +89,19 @@ typedef PopPageCallback = bool Function(Route route, dynamic result); 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 @@ abstract class Route { /// 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 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 @@ class Navigator extends StatefulWidget { /// 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 @@ class Navigator extends StatefulWidget { 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 @@ class _RouteEntry extends RouteTransitionRecord { assert(isPresent); pendingResult = result; currentState = _RouteLifecycle.pop; - route.onPopInvoked(true); } bool _reportRemovalToObserver = true; @@ -3348,78 +3295,12 @@ class _NavigatorReplaceObservation extends _NavigatorObservation { } } -typedef _IndexWhereCallback = bool Function(_RouteEntry element); - -/// A collection of _RouteEntries representing a navigation history. -/// -/// Acts as a ChangeNotifier and notifies after its List of _RouteEntries is -/// mutated. -class _History extends Iterable<_RouteEntry> with ChangeNotifier { - 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; - } - - _RouteEntry operator [](int index) { - return _value[index]; - } - - @override - Iterator<_RouteEntry> get iterator { - return _value.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 with TickerProviderStateMixin, RestorationMixin { late GlobalKey _overlayKey; - final _History _history = _History(); - + List<_RouteEntry> _history = <_RouteEntry>[]; /// A set for entries that are waiting to dispose until their subtrees are /// disposed. /// @@ -3449,43 +3330,12 @@ class NavigatorState extends State with TickerProviderStateMixin, Res late List _effectiveObservers; - bool get _usingPagesAPI => widget.pages != const >[]; - - 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 >[]) { + // This navigator uses page API. if (widget.pages.isEmpty) { FlutterError.reportError( FlutterErrorDetails( @@ -3528,8 +3378,6 @@ class NavigatorState extends State with TickerProviderStateMixin, Res if (widget.reportsRouteUpdateToEngine) { SystemNavigator.selectSingleEntryHistory(); } - - _history.addListener(_handleHistoryChanged); } // Use [_nextPagelessRestorationScopeId] to get the next id. @@ -3712,7 +3560,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res void didUpdateWidget(Navigator oldWidget) { super.didUpdateWidget(oldWidget); assert(() { - if (_usingPagesAPI) { + if (widget.pages != const >[]) { // This navigator uses page API. if (widget.pages.isEmpty) { FlutterError.reportError( @@ -3824,8 +3672,6 @@ class NavigatorState extends State with TickerProviderStateMixin, Res _rawNextPagelessRestorationScopeId.dispose(); _serializableHistory.dispose(); userGestureInProgressNotifier.dispose(); - _history.removeListener(_handleHistoryChanged); - _history.dispose(); super.dispose(); // don't unlock, so that the object becomes unusable assert(_debugLocked); @@ -4111,7 +3957,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res 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]!); @@ -5127,17 +4973,17 @@ class NavigatorState extends State with TickerProviderStateMixin, Res 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 maybePop([ T? result ]) async { @@ -5146,31 +4992,23 @@ class NavigatorState extends State with TickerProviderStateMixin, Res 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; } } @@ -5460,46 +5298,29 @@ class NavigatorState extends State with TickerProviderStateMixin, Res 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( - 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 [], - ), + 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 [], ), ), ), @@ -5660,7 +5481,7 @@ class _HistoryProperty extends RestorableProperty>?> { // Updating. - void update(_History history) { + void update(List<_RouteEntry> history) { assert(isRegistered); final bool wasUninitialized = _pageToPagelessRoutes == null; bool needsSerialization = wasUninitialized; @@ -5983,26 +5804,3 @@ class RestorableRouteFuture extends RestorableProperty { 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 203a85bede..0000000000 --- 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 createState() => _NavigatorPopHandlerState(); -} - -class _NavigatorPopHandlerState extends State { - 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( - 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 b47d83fcdb..0000000000 --- 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 createState() => _PopScopeState(); -} - -class _PopScopeState extends State implements PopEntry { - ModalRoute? _route; - - @override - PopInvokedCallback? get onPopInvoked => widget.onPopInvoked; - - @override - late final ValueNotifier canPopNotifier; - - @override - void initState() { - super.initState(); - canPopNotifier = ValueNotifier(widget.canPop); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final ModalRoute? 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 e54e46ab3c..441486e5ed 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -717,10 +717,6 @@ mixin LocalHistoryRoute on Route { } } - @Deprecated( - 'Use popDisposition instead. ' - 'This feature was deprecated after v3.12.0-1.0.pre.', - ) @override Future willPop() async { if (willHandlePopInternally) { @@ -729,14 +725,6 @@ mixin LocalHistoryRoute on Route { return super.willPop(); } - @override - RoutePopDisposition get popDisposition { - if (willHandlePopInternally) { - return RoutePopDisposition.pop; - } - return super.popDisposition; - } - @override bool didPop(T? result) { if (_localHistory != null && _localHistory!.isNotEmpty) { @@ -1502,8 +1490,6 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute _willPopCallbacks = []; - final Set _popEntries = {}; - /// 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 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute willPop() async { final _ModalScopeState? scope = _scopeKey.currentState; @@ -1538,44 +1520,26 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute { + /// ModalRoute? _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 { + /// ModalRoute? _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 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute extends TransitionRoute with LocalHistoryRoute '${objectRuntimeType(this, 'ModalRoute')}($settings, animation: $_animation)'; } @@ -2279,33 +2212,3 @@ typedef RoutePageBuilder = Widget Function(BuildContext context, Animation animation, Animation 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 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 eefe437983..ab90c7f49d 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 @@ import 'routes.dart'; /// 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 3ca0999b99..539d6aac62 100644 --- a/packages/flutter/lib/widgets.dart +++ b/packages/flutter/lib/widgets.dart @@ -81,7 +81,6 @@ export 'src/widgets/media_query.dart'; 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/placeholder.dart'; 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 b376a92ecf..a8a43b5102 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 selectedTabs; @@ -1216,132 +1215,6 @@ void main() { 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()); - 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( - 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.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 d403ceba9c..26d1463639 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -6,12 +6,9 @@ import 'dart:ui' show FlutterView; 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 @@ void main() { 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()); - 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.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: { - '/': (BuildContext context) => _LinksPage( - title: 'Home page', - buttons: [ - TextButton( - onPressed: () { - Navigator.of(context).pushNamed('/one'); - }, - child: const Text('Go to one'), - ), - ], - ), - '/one': (BuildContext context) => _LinksPage( - title: 'Page one', - buttons: [ - 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.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: { - '/': (BuildContext context) => _LinksPage( - title: 'Home page', - buttons: [ - TextButton( - onPressed: () { - Navigator.of(context).pushNamed('/one'); - }, - child: const Text('Go to one'), - ), - ], - ), - '/one': (BuildContext context) => _LinksPage( - title: 'Page one', - buttons: [ - 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.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: { - '/': (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.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: { - '/': (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.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 nav = GlobalKey(); - final GlobalKey nestedNav = GlobalKey(); - Future 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: { - '/': (BuildContext context) => _LinksPage( - title: 'Home page', - buttons: [ - 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: [ - 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.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: { - '/': (BuildContext context) => _LinksPage( - title: 'Home page', - buttons: [ - 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: [ - 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.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 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( - child: _LinksPage( - title: 'Home page', - buttons: [ - 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( - child: _LinksPage( - title: 'Page one', - ), - ); - case _Page.noPop: - return const MaterialPage( - 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.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 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( - child: _LinksPage( - title: 'Home page', - buttons: [ - 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( - child: _LinksPage( - title: 'Page one', - ), - ); - case _Page.noPop: - return const MaterialPage( - 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.android }), - skip: isBrowser, // [intended] only non-web Android supports predictive back. - ); - }); - }); } typedef AnnouncementCallBack = void Function(Route?); @@ -5151,153 +4435,3 @@ class TestDependencies extends StatelessWidget { ); } } - -enum _BackType { - systemBack, - navigatorPop, -} - -enum _Page { - home, - one, - noPop, -} - -class _LinksPage extends StatelessWidget { - const _LinksPage ({ - this.buttons = const [], - this.canPop, - required this.title, - this.onBack, - }); - - final List 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: [ - 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? navigatorKey; - - @override - State<_NestedNavigatorsPage> createState() => _NestedNavigatorsPageState(); -} - -class _NestedNavigatorsPageState extends State<_NestedNavigatorsPage> { - late final GlobalKey _navigatorKey; - - @override - void initState() { - super.initState(); - _navigatorKey = widget.navigatorKey ?? GlobalKey(); - } - - @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( - builder: (BuildContext context) { - return _LinksPage( - title: 'Nested - home', - onBack: () { - Navigator.of(rootContext).pop(); - }, - buttons: [ - 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( - builder: (BuildContext context) { - return const _LinksPage( - title: 'Nested - page one', - ); - }, - ); - case '/popscope': - return MaterialPageRoute( - 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 46f1f9b1ac..0000000000 --- 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 simulateSystemBack() { - return TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - 'flutter/navigation', - const JSONMessageCodec().encodeMessage({ - '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 c5d0e88545..0000000000 --- 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()); - 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: { - '/': (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: [ - 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 nav = GlobalKey(); - bool canPop = true; - late StateSetter setState; - late BuildContext homeContext; - late BuildContext oneContext; - late bool lastPopSuccess; - await tester.pumpWidget( - MaterialApp( - navigatorKey: nav, - initialRoute: '/', - routes: { - '/': (BuildContext context) { - homeContext = context; - return Scaffold( - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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: [ - 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: { - '/': (BuildContext buildContext) => Scaffold( - body: StatefulBuilder( - builder: (BuildContext buildContext, StateSetter stateSetter) { - context = buildContext; - setState = stateSetter; - const Widget child = Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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: [ - 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(), - ); -}