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 a68c2a194c9b..a063c7cfe4dc 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,7 +48,7 @@ class CupertinoNavigationDemo extends StatelessWidget { @override Widget build(BuildContext context) { - return PopScope( + return PopScope( // Prevent swipe popping of this page. Use explicit exit buttons only. canPop: false, child: DefaultTextStyle( 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 81ee4dacd864..140211387c7c 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 @@ -110,7 +110,7 @@ class FullScreenDialogDemoState extends State { bool _hasName = false; late String _eventName; - Future _handlePopInvoked(bool didPop) async { + Future _handlePopInvoked(bool didPop, Object? result) async { if (didPop) { return; } @@ -175,7 +175,7 @@ class FullScreenDialogDemoState extends State { ), body: Form( canPop: !_saveNeeded && !_hasLocation && !_hasName, - onPopInvoked: _handlePopInvoked, + onPopInvokedWithResult: _handlePopInvoked, 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 c6f644ee74cd..47811d81c51e 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,7 +143,7 @@ class TextFormFieldDemoState extends State { return null; } - Future _handlePopInvoked(bool didPop) async { + Future _handlePopInvoked(bool didPop, Object? result) async { if (didPop) { return; } @@ -192,7 +192,7 @@ class TextFormFieldDemoState extends State { key: _formKey, autovalidateMode: _autovalidateMode, canPop: _formKey.currentState == null || !_formWasEdited || _formKey.currentState!.validate(), - onPopInvoked: _handlePopInvoked, + onPopInvokedWithResult: _handlePopInvoked, 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 5e7a95166859..0dfbcbe975de 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 @@ -355,7 +355,7 @@ 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) { + void _handlePopInvoked(bool didPop, Object? result) { if (didPop) { return; } @@ -370,9 +370,9 @@ class ExpandingBottomSheetState extends State with TickerP duration: const Duration(milliseconds: 225), curve: Curves.easeInOut, alignment: FractionalOffset.topLeft, - child: PopScope( + child: PopScope( canPop: !_isOpen, - onPopInvoked: _handlePopInvoked, + onPopInvokedWithResult: _handlePopInvoked, 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 a6ceab8850b0..fc7c4d800587 100644 --- a/dev/integration_tests/flutter_gallery/lib/gallery/home.dart +++ b/dev/integration_tests/flutter_gallery/lib/gallery/home.dart @@ -326,9 +326,9 @@ class _GalleryHomeState extends State with SingleTickerProviderStat backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor, body: SafeArea( bottom: false, - child: PopScope( + child: PopScope( canPop: _category == null, - onPopInvoked: (bool didPop) { + onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } diff --git a/examples/api/lib/widgets/form/form.1.dart b/examples/api/lib/widgets/form/form.1.dart index e008f5aaa4d2..a7685152ab23 100644 --- a/examples/api/lib/widgets/form/form.1.dart +++ b/examples/api/lib/widgets/form/form.1.dart @@ -111,7 +111,7 @@ class _SaveableFormState extends State<_SaveableForm> { const SizedBox(height: 20.0), Form( canPop: !_isDirty, - onPopInvoked: (bool didPop) async { + onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { return; } diff --git a/examples/api/lib/widgets/pop_scope/pop_scope.0.dart b/examples/api/lib/widgets/pop_scope/pop_scope.0.dart index 2400b0905e01..e2ed446259e6 100644 --- a/examples/api/lib/widgets/pop_scope/pop_scope.0.dart +++ b/examples/api/lib/widgets/pop_scope/pop_scope.0.dart @@ -109,9 +109,9 @@ class _PageTwoState extends State<_PageTwo> { mainAxisAlignment: MainAxisAlignment.center, children: [ const Text('Page Two'), - PopScope( + PopScope( canPop: false, - onPopInvoked: (bool didPop) async { + onPopInvokedWithResult: (bool didPop, Object? result) async { if (didPop) { return; } diff --git a/examples/api/lib/widgets/pop_scope/pop_scope.1.dart b/examples/api/lib/widgets/pop_scope/pop_scope.1.dart new file mode 100644 index 000000000000..7a058837a548 --- /dev/null +++ b/examples/api/lib/widgets/pop_scope/pop_scope.1.dart @@ -0,0 +1,233 @@ +// 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 how to use a PopScope to wrap a widget that +// may pop the page with a result. + +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', + onGenerateRoute: (RouteSettings settings) { + return switch (settings.name) { + '/two' => MaterialPageRoute( + builder: (BuildContext context) => const _PageTwo(), + ), + _ => MaterialPageRoute( + builder: (BuildContext context) => const _HomePage(), + ), + }; + }, + ); + } +} + +class _HomePage extends StatefulWidget { + const _HomePage(); + + @override + State<_HomePage> createState() => _HomePageState(); +} + +class _HomePageState extends State<_HomePage> { + FormData? _formData; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Page One'), + if (_formData != null) + Text('Hello ${_formData!.name}, whose favorite food is ${_formData!.favoriteFood}.'), + TextButton( + onPressed: () async { + final FormData formData = + await Navigator.of(context).pushNamed('/two') + ?? const FormData(); + if (formData != _formData) { + setState(() { + _formData = formData; + }); + } + }, + child: const Text('Next page'), + ), + ], + ), + ), + ); + } +} + +class _PopScopeWrapper extends StatelessWidget { + const _PopScopeWrapper({required this.child}); + + final Widget child; + + Future _showBackDialog(BuildContext context) { + return 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('Never mind'), + onPressed: () { + Navigator.pop(context, false); + }, + ), + TextButton( + style: TextButton.styleFrom( + textStyle: Theme.of(context).textTheme.labelLarge, + ), + child: const Text('Leave'), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return PopScope( + canPop: false, + // The result argument contains the pop result that is defined in `_PageTwo`. + onPopInvokedWithResult: (bool didPop, FormData? result) async { + if (didPop) { + return; + } + final bool shouldPop = await _showBackDialog(context) ?? false; + if (context.mounted && shouldPop) { + Navigator.pop(context, result); + } + }, + child: child, + ); + } +} + +// This is a PopScope wrapper over _PageTwoBody +class _PageTwo extends StatelessWidget { + const _PageTwo(); + + @override + Widget build(BuildContext context) { + return const _PopScopeWrapper( + child: _PageTwoBody(), + ); + } + +} + +class _PageTwoBody extends StatefulWidget { + const _PageTwoBody(); + + @override + State<_PageTwoBody> createState() => _PageTwoBodyState(); +} + +class _PageTwoBodyState extends State<_PageTwoBody> { + FormData _formData = const FormData(); + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Page Two'), + Form( + child: Column( + children: [ + TextFormField( + decoration: const InputDecoration( + hintText: 'Enter your name.', + ), + onChanged: (String value) { + _formData = _formData.copyWith( + name: value, + ); + }, + ), + TextFormField( + decoration: const InputDecoration( + hintText: 'Enter your favorite food.', + ), + onChanged: (String value) { + _formData = _formData.copyWith( + favoriteFood: value, + ); + }, + ), + ], + ), + ), + TextButton( + onPressed: () async { + Navigator.maybePop(context, _formData); + }, + child: const Text('Go back'), + ), + ], + ), + ), + ); + } +} + +@immutable +class FormData { + const FormData({ + this.name = '', + this.favoriteFood = '', + }); + + final String name; + final String favoriteFood; + + FormData copyWith({String? name, String? favoriteFood}) { + return FormData( + name: name ?? this.name, + favoriteFood: favoriteFood ?? this.favoriteFood, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is FormData + && other.name == name + && other.favoriteFood == favoriteFood; + } + + @override + int get hashCode => Object.hash(name, favoriteFood); +} diff --git a/examples/api/test/widgets/pop_scope/pop_scope.1_test.dart b/examples/api/test/widgets/pop_scope/pop_scope.1_test.dart new file mode 100644 index 000000000000..14266af52147 --- /dev/null +++ b/examples/api/test/widgets/pop_scope/pop_scope.1_test.dart @@ -0,0 +1,67 @@ +// 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/pop_scope/pop_scope.1.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); + + await tester.tap(find.text('Next page')); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + + 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('Never mind')); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + }); + + testWidgets('Can choose to go back with pop result', (WidgetTester tester) async { + await tester.pumpWidget( + const example.NavigatorPopHandlerApp(), + ); + + expect(find.text('Page One'), findsOneWidget); + expect(find.text('Page Two'), findsNothing); + + await tester.tap(find.text('Next page')); + await tester.pumpAndSettle(); + expect(find.text('Page One'), findsNothing); + expect(find.text('Page Two'), findsOneWidget); + + await tester.enterText(find.byType(TextFormField).first, 'John'); + await tester.pumpAndSettle(); + await tester.enterText(find.byType(TextFormField).last, 'Apple'); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Go back')); + 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); + expect(find.text('Hello John, whose favorite food is Apple.'), findsOneWidget); + }); +} diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index a61f3a0d7974..67c1a1ae1323 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -1230,9 +1230,9 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp } MaterialPageRoute _detailPageRoute(Object? arguments) { - return MaterialPageRoute(builder: (BuildContext context) { - return PopScope( - onPopInvoked: (bool didPop) { + return MaterialPageRoute(builder: (BuildContext context) { + return PopScope( + onPopInvokedWithResult: (bool didPop, void result) { // No need for setState() as rebuild happens on navigation pop. focus = _Focus.master; }, diff --git a/packages/flutter/lib/src/widgets/form.dart b/packages/flutter/lib/src/widgets/form.dart index 5277e8a60331..b0b322e8f391 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -55,16 +55,22 @@ class Form extends StatefulWidget { super.key, required this.child, this.canPop, + @Deprecated( + 'Use onPopInvokedWithResult instead. ' + 'This feature was deprecated after v3.22.0-12.0.pre.', + ) this.onPopInvoked, + this.onPopInvokedWithResult, @Deprecated( - 'Use canPop and/or onPopInvoked instead. ' + 'Use canPop and/or onPopInvokedWithResult 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.'); + assert(onPopInvokedWithResult == null || onPopInvoked == null, 'onPopInvoked is deprecated; use onPopInvokedWithResult'), + assert(((onPopInvokedWithResult ?? onPopInvoked ?? canPop) == null) || onWillPop == null, 'onWillPop is deprecated; use canPop and/or onPopInvokedWithResult.'); /// Returns the [FormState] of the closest [Form] widget which encloses the /// given context, or null if none is found. @@ -144,7 +150,7 @@ class Form extends StatefulWidget { /// * [WillPopScope], another widget that provides a way to intercept the /// back button. @Deprecated( - 'Use canPop and/or onPopInvoked instead. ' + 'Use canPop and/or onPopInvokedWithResult instead. ' 'This feature was deprecated after v3.12.0-1.0.pre.', ) final WillPopCallback? onWillPop; @@ -160,12 +166,19 @@ class Form extends StatefulWidget { /// /// See also: /// - /// * [onPopInvoked], which also comes from [PopScope] and is often used in + /// * [onPopInvokedWithResult], 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} + /// {@macro flutter.widgets.navigator.onPopInvokedWithResult} + @Deprecated( + 'Use onPopInvokedWithResult instead. ' + 'This feature was deprecated after v3.22.0-12.0.pre.', + ) + final PopInvokedCallback? onPopInvoked; + + /// {@macro flutter.widgets.navigator.onPopInvokedWithResult} /// /// {@tool dartpad} /// This sample demonstrates how to use this parameter to show a confirmation @@ -178,8 +191,8 @@ class Form extends StatefulWidget { /// /// * [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; + /// * [PopScope.onPopInvokedWithResult], which is what [Form] delegates to internally. + final PopInvokedWithResultCallback? onPopInvokedWithResult; /// Called when one of the form fields changes. /// @@ -193,6 +206,14 @@ class Form extends StatefulWidget { /// {@macro flutter.widgets.FormField.autovalidateMode} final AutovalidateMode autovalidateMode; + void _callPopInvoked(bool didPop, Object? result) { + if (onPopInvokedWithResult != null) { + onPopInvokedWithResult!(didPop, result); + return; + } + onPopInvoked?.call(didPop); + } + @override FormState createState() => FormState(); } @@ -258,10 +279,10 @@ class FormState extends State
{ break; } - if (widget.canPop != null || widget.onPopInvoked != null) { - return PopScope( + if (widget.canPop != null || (widget.onPopInvokedWithResult ?? widget.onPopInvoked) != null) { + return PopScope( canPop: widget.canPop ?? true, - onPopInvoked: widget.onPopInvoked, + onPopInvokedWithResult: widget._callPopInvoked, child: _FormScope( formState: this, generation: _generation, diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 662f12d335c7..b5f9c48f250d 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -348,15 +348,26 @@ abstract class Route extends _RoutePlaceholder { 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} + @Deprecated( + 'Override onPopInvokedWithResult instead. ' + 'This feature was deprecated after v3.22.0-12.0.pre.', + ) void onPopInvoked(bool didPop) {} + /// {@template flutter.widgets.navigator.onPopInvokedWithResult} + /// 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 onPopInvokedWithResult(bool didPop, T? result) => onPopInvoked(didPop); + /// Whether calling [didPop] would return false. bool get willHandlePopInternally => false; @@ -3109,7 +3120,7 @@ class _RouteEntry extends RouteTransitionRecord { assert(isPresent); pendingResult = result; currentState = _RouteLifecycle.pop; - route.onPopInvoked(true); + route.onPopInvokedWithResult(true, result); } bool _reportRemovalToObserver = true; @@ -5239,7 +5250,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res pop(result); return true; case RoutePopDisposition.doNotPop: - lastEntry.route.onPopInvoked(false); + lastEntry.route.onPopInvokedWithResult(false, result); return true; } } @@ -5282,7 +5293,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res assert(entry.route._popCompleter.isCompleted); entry.currentState = _RouteLifecycle.pop; } - entry.route.onPopInvoked(true); + entry.route.onPopInvokedWithResult(true, result); } else { entry.pop(result); assert (entry.currentState == _RouteLifecycle.pop); diff --git a/packages/flutter/lib/src/widgets/navigator_pop_handler.dart b/packages/flutter/lib/src/widgets/navigator_pop_handler.dart index 203a85beded1..ea2be6695551 100644 --- a/packages/flutter/lib/src/widgets/navigator_pop_handler.dart +++ b/packages/flutter/lib/src/widgets/navigator_pop_handler.dart @@ -81,9 +81,9 @@ class _NavigatorPopHandlerState extends State { 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( + return PopScope( canPop: !widget.enabled || _canPop, - onPopInvoked: (bool didPop) { + onPopInvokedWithResult: (bool didPop, Object? result) { if (didPop) { return; } diff --git a/packages/flutter/lib/src/widgets/pop_scope.dart b/packages/flutter/lib/src/widgets/pop_scope.dart index c8b31f60c8d5..23e8300cd9c9 100644 --- a/packages/flutter/lib/src/widgets/pop_scope.dart +++ b/packages/flutter/lib/src/widgets/pop_scope.dart @@ -8,12 +8,29 @@ import 'framework.dart'; import 'navigator.dart'; import 'routes.dart'; +/// 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. +@Deprecated( + 'Use PopWithResultInvokedCallback instead. ' + 'This feature was deprecated after v3.22.0-12.0.pre.', +) +typedef PopInvokedCallback = void Function(bool didPop); + /// Manages back navigation gestures. /// +/// The generic type should match or be a supertype of the generic type of the +/// enclosing [Route]. For example, if the enclosing Route is a +/// `MaterialPageRoute`, you can define [PopScope] with `int` or any +/// supertype of `int`. +/// /// The [canPop] parameter disables back gestures when set to `false`. /// -/// The [onPopInvoked] parameter reports when pop navigation was attempted, and -/// `didPop` indicates whether or not the navigation was successful. +/// The [onPopInvokedWithResult] parameter reports when pop navigation was attempted, and +/// `didPop` indicates whether or not the navigation was successful. The +/// `result` contains the pop result. /// /// Android has a system back gesture that is a swipe inward from near the edge /// of the screen. It is recognized by Android before being passed to Flutter. @@ -22,15 +39,15 @@ import 'routes.dart'; /// back gesture. /// /// 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 +/// of the enclosing [Navigator]. [onPopInvokedWithResult] will still be called, and /// `didPop` will be `false`. On iOS when using [CupertinoRouteTransitionMixin] /// with [canPop] set to false, no gesture will be detected at all, so -/// [onPopInvoked] will not be called. Programmatically attempting pop -/// navigation will also result in a call to [onPopInvoked], with `didPop` +/// [onPopInvokedWithResult] will not be called. Programmatically attempting pop +/// navigation will also result in a call to [onPopInvokedWithResult], with `didPop` /// indicating success or failure. /// /// 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 +/// [Navigator] to receive a pop as usual. [onPopInvokedWithResult] will be called with /// `didPop` as true, unless the pop failed for reasons unrelated to /// [PopScope], in which case it will be false. /// @@ -41,30 +58,43 @@ import 'routes.dart'; /// ** See code in examples/api/lib/widgets/pop_scope/pop_scope.0.dart ** /// {@end-tool} /// +/// {@tool dartpad} +/// This sample demonstrates showing how to use PopScope to wrap widget that +/// may pop the page with a result. +/// +/// ** See code in examples/api/lib/widgets/pop_scope/pop_scope.1.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 +/// * [Form.canPop] and [Form.onPopInvokedWithResult], 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 { +@optionalTypeArgs +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.onPopInvokedWithResult, + @Deprecated( + 'Use onPopInvokedWithResult instead. ' + 'This feature was deprecated after v3.22.0-12.0.pre.', + ) this.onPopInvoked, - }); + }) : assert(onPopInvokedWithResult == null || onPopInvoked == null, 'onPopInvoked is deprecated, use onPopInvokedWithResult'); /// The widget below this widget in the tree. /// /// {@macro flutter.widgets.ProxyWidget.child} final Widget child; - /// {@template flutter.widgets.PopScope.onPopInvoked} + /// {@template flutter.widgets.PopScope.onPopInvokedWithResult} /// Called after a route pop was handled. /// {@endtemplate} /// @@ -78,11 +108,38 @@ class PopScope extends StatefulWidget { /// indicates whether or not the back navigation actually happened /// successfully. /// + /// The `result` contains the pop result. + /// /// See also: /// - /// * [Route.onPopInvoked], which is similar. + /// * [Route.onPopInvokedWithResult], which is similar. + final PopInvokedWithResultCallback? onPopInvokedWithResult; + + /// Called after a route pop was handled. + /// + /// 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. + @Deprecated( + 'Use onPopInvokedWithResult instead. ' + 'This feature was deprecated after v3.22.0-12.0.pre.', + ) final PopInvokedCallback? onPopInvoked; + void _callPopInvoked(bool didPop, T? result) { + if (onPopInvokedWithResult != null) { + onPopInvokedWithResult!(didPop, result); + return; + } + onPopInvoked?.call(didPop); + } + /// {@template flutter.widgets.PopScope.canPop} /// When false, blocks the current route from being popped. /// @@ -99,14 +156,21 @@ class PopScope extends StatefulWidget { final bool canPop; @override - State createState() => _PopScopeState(); + State> createState() => _PopScopeState(); } -class _PopScopeState extends State implements PopEntry { +class _PopScopeState extends State> implements PopEntry { ModalRoute? _route; @override - PopInvokedCallback? get onPopInvoked => widget.onPopInvoked; + void onPopInvoked(bool didPop) { + throw UnimplementedError(); + } + + @override + void onPopInvokedWithResult(bool didPop, T? result) { + widget._callPopInvoked(didPop, result); + } @override late final ValueNotifier canPopNotifier; @@ -129,7 +193,7 @@ class _PopScopeState extends State implements PopEntry { } @override - void didUpdateWidget(PopScope oldWidget) { + void didUpdateWidget(PopScope oldWidget) { super.didUpdateWidget(oldWidget); canPopNotifier.value = widget.canPop; } diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart index 6e16448d6e2a..174d023999fb 100644 --- a/packages/flutter/lib/src/widgets/routes.dart +++ b/packages/flutter/lib/src/widgets/routes.dart @@ -1669,7 +1669,9 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute _willPopCallbacks = []; - final Set _popEntries = {}; + // Holding as Object? instead of T so that PopScope in this route can be + // declared with any supertype of T. + final Set> _popEntries = >{}; /// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with /// [addScopedWillPopCallback] returns either false or null. If they all @@ -1717,14 +1719,14 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute popEntry in _popEntries) { if (!popEntry.canPopNotifier.value) { return RoutePopDisposition.doNotPop; } @@ -1734,9 +1736,9 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute popEntry in _popEntries) { + popEntry.onPopInvokedWithResult(didPop, result); } } @@ -1786,14 +1788,14 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute popEntry) { _popEntries.add(popEntry); popEntry.canPopNotifier.addListener(_handlePopEntryChange); _handlePopEntryChange(); @@ -1804,7 +1806,7 @@ abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute popEntry) { _popEntries.remove(popEntry); popEntry.canPopNotifier.removeListener(_handlePopEntryChange); _handlePopEntryChange(); @@ -2413,11 +2415,13 @@ typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animatio /// /// Accepts a didPop boolean indicating whether or not back navigation /// succeeded. -typedef PopInvokedCallback = void Function(bool didPop); +/// +/// The `result` contains the pop result. +typedef PopInvokedWithResultCallback = void Function(bool didPop, T? result); /// Allows listening to and preventing pops. /// -/// Can be registered in [ModalRoute] to listen to pops with [onPopInvoked] or +/// Can be registered in [ModalRoute] to listen to pops with [onPopInvokedWithResult] or /// to enable/disable them with [canPopNotifier]. /// /// See also: @@ -2425,15 +2429,23 @@ typedef PopInvokedCallback = void Function(bool didPop); /// * [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; +abstract class PopEntry { + + /// {@macro flutter.widgets.PopScope.onPopInvokedWithResult} + @Deprecated( + 'Use onPopInvokedWithResult instead. ' + 'This feature was deprecated after v3.22.0-12.0.pre.', + ) + void onPopInvoked(bool didPop) { } + + /// {@macro flutter.widgets.PopScope.onPopInvokedWithResult} + void onPopInvokedWithResult(bool didPop, T? result) => onPopInvoked(didPop); /// {@macro flutter.widgets.PopScope.canPop} ValueListenable get canPopNotifier; @override String toString() { - return 'PopEntry canPop: ${canPopNotifier.value}, onPopInvoked: $onPopInvoked'; + return 'PopEntry canPop: ${canPopNotifier.value}, onPopInvoked: $onPopInvokedWithResult'; } } diff --git a/packages/flutter/test/cupertino/tab_test.dart b/packages/flutter/test/cupertino/tab_test.dart index 5acf173cd634..7188777509ac 100644 --- a/packages/flutter/test/cupertino/tab_test.dart +++ b/packages/flutter/test/cupertino/tab_test.dart @@ -305,7 +305,7 @@ void main() { BottomNavigationBarItem(label: '', icon: Text('2')) ], ), - tabBuilder: (_, int i) => PopScope( + tabBuilder: (_, int i) => PopScope( canPop: false, child: CupertinoTabView( navigatorKey: key, diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 0bb9ba9d5d73..3407f61dcf56 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -2989,7 +2989,7 @@ void main() { const List> myPages = >[ MaterialPage(child: Text('page1')), MaterialPage( - child: PopScope( + child: PopScope( canPop: false, child: Text('page2'), ), @@ -4908,9 +4908,9 @@ void main() { home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { builderSetState = setState; - return PopScope( + return PopScope( canPop: canPop(), - onPopInvoked: (bool success) { + onPopInvokedWithResult: (bool success, Object? result) { if (success || pages.last == _Page.noPop) { return; } @@ -5024,9 +5024,9 @@ void main() { MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return PopScope( + return PopScope( canPop: canPop(), - onPopInvoked: (bool success) { + onPopInvokedWithResult: (bool success, Object? result) { if (success || pages.last == _Page.noPop) { return; } @@ -5117,9 +5117,9 @@ void main() { MaterialApp( home: StatefulBuilder( builder: (BuildContext context, StateSetter setState) { - return PopScope( + return PopScope( canPop: canPop(), - onPopInvoked: (bool success) { + onPopInvokedWithResult: (bool success, Object? result) { if (success || pages.last == _PageWithYesPop.noPop) { return; } @@ -5189,7 +5189,7 @@ void main() { child: _LinksPage( title: 'Can pop page', canPop: true, - onPopInvoked: (bool didPop) { + onPopInvoked: (bool didPop, void result) { onPopInvokedCallCount += 1; }, ), @@ -5556,7 +5556,7 @@ class _LinksPage extends StatelessWidget { final bool? canPop; final VoidCallback? onBack; final String title; - final PopInvokedCallback? onPopInvoked; + final PopInvokedWithResultCallback? onPopInvoked; @override Widget build(BuildContext context) { @@ -5575,9 +5575,9 @@ class _LinksPage extends StatelessWidget { child: const Text('Go back'), ), if (canPop != null) - PopScope( + PopScope( canPop: canPop!, - onPopInvoked: onPopInvoked, + onPopInvokedWithResult: onPopInvoked, child: const SizedBox.shrink(), ), ], diff --git a/packages/flutter/test/widgets/pop_scope_test.dart b/packages/flutter/test/widgets/pop_scope_test.dart index 116951ce7877..8e6d1040533e 100644 --- a/packages/flutter/test/widgets/pop_scope_test.dart +++ b/packages/flutter/test/widgets/pop_scope_test.dart @@ -47,7 +47,7 @@ void main() { builder: (BuildContext buildContext, StateSetter stateSetter) { context = buildContext; setState = stateSetter; - return PopScope( + return PopScope( canPop: canPop, child: const Center( child: Column( @@ -79,6 +79,94 @@ void main() { variant: TargetPlatformVariant.all(), ); + testWidgets('pop scope can receive result', (WidgetTester tester) async { + Object? receivedResult; + final Object poppedResult = Object(); + final GlobalKey nav = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + initialRoute: '/', + navigatorKey: nav, + home: Scaffold( + body: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + receivedResult = result; + }, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Home/PopScope Page'), + ], + ), + ), + ), + ), + ), + ); + + nav.currentState!.maybePop(poppedResult); + await tester.pumpAndSettle(); + expect(receivedResult, poppedResult); + }, + variant: TargetPlatformVariant.all(), + ); + + testWidgets('pop scope can have Object? generic type while route has stricter generic type', (WidgetTester tester) async { + Object? receivedResult; + const int poppedResult = 13; + final GlobalKey nav = GlobalKey(); + await tester.pumpWidget( + MaterialApp( + initialRoute: '/', + navigatorKey: nav, + home: Scaffold( + body: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + receivedResult = result; + }, + child: const Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('Home/PopScope Page'), + ], + ), + ), + ), + ), + ), + ); + + nav.currentState!.push( + MaterialPageRoute( + builder: (BuildContext context) { + return Scaffold( + body: PopScope( + canPop: false, + onPopInvokedWithResult: (bool didPop, Object? result) { + receivedResult = result; + }, + child: const Center( + child: Text('new page'), + ), + ), + ); + }, + ), + ); + await tester.pumpAndSettle(); + expect(find.text('new page'), findsOneWidget); + + nav.currentState!.maybePop(poppedResult); + await tester.pumpAndSettle(); + expect(receivedResult, poppedResult); + }, + variant: TargetPlatformVariant.all(), + ); + testWidgets('toggling canPop on secondary route allows/prevents backs', (WidgetTester tester) async { final GlobalKey nav = GlobalKey(); bool canPop = true; @@ -115,9 +203,9 @@ void main() { builder: (BuildContext context, StateSetter stateSetter) { oneContext = context; setState = stateSetter; - return PopScope( + return PopScope( canPop: canPop, - onPopInvoked: (bool didPop) { + onPopInvokedWithResult: (bool didPop, Object? result) { lastPopSuccess = didPop; }, child: const Center( @@ -271,7 +359,7 @@ void main() { if (!usePopScope) { return child; } - return const PopScope( + return const PopScope( canPop: false, child: child, ); @@ -314,12 +402,12 @@ void main() { return Column( children: [ if (usePopScope1) - const PopScope( + const PopScope( canPop: false, child: Text('hello'), ), if (usePopScope2) - const PopScope( + const PopScope( canPop: false, child: Text('hello'), ),