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..a063c7cfe4 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 81ee4dacd8..86c5773111 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; } 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..dd479d5222 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; } 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 5e7a951668..1a90c14584 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,7 +370,7 @@ 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, child: AnimatedBuilder( diff --git a/dev/integration_tests/flutter_gallery/lib/gallery/home.dart b/dev/integration_tests/flutter_gallery/lib/gallery/home.dart index a6ceab8850..1e57e39e38 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) { + onPopInvoked: (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 e008f5aaa4..c6d602dc9f 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 { + onPopInvoked: (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 2400b0905e..c86640c38a 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 { + onPopInvoked: (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 0000000000..cb3b9f2574 --- /dev/null +++ b/examples/api/lib/widgets/pop_scope/pop_scope.1.dart @@ -0,0 +1,232 @@ +// 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 how to use PopScope to wrap 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 contains pop result in `_PageTwo`. + onPopInvoked: (bool didPop, FormData? result) async { + if (didPop) { + return; + } + final bool shouldPop = await _showBackDialog(context) ?? false; + if (context.mounted && shouldPop) { + Navigator.pop(context, result); + } + }, + child: const _PageTwoBody(), + ); + } +} + +// 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 0000000000..14266af521 --- /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 a61f3a0d79..308a9d8c7d 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( + onPopInvoked: (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 2211523a4a..823235b96f 100644 --- a/packages/flutter/lib/src/widgets/form.dart +++ b/packages/flutter/lib/src/widgets/form.dart @@ -177,7 +177,7 @@ 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; + final PopInvokedCallback? onPopInvoked; /// Called when one of the form fields changes. /// @@ -244,7 +244,7 @@ class FormState extends State
{ } if (widget.canPop != null || widget.onPopInvoked != null) { - return PopScope( + return PopScope( canPop: widget.canPop ?? true, onPopInvoked: widget.onPopInvoked, child: _FormScope( diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index 88b869baf5..edf3eb1140 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -355,7 +355,7 @@ abstract class Route extends _RoutePlaceholder { /// will still be called. The `didPop` parameter indicates whether or not the /// back navigation actually happened successfully. /// {@endtemplate} - void onPopInvoked(bool didPop) {} + void onPopInvoked(bool didPop, T? result) {} /// Whether calling [didPop] would return false. bool get willHandlePopInternally => false; @@ -3109,7 +3109,7 @@ class _RouteEntry extends RouteTransitionRecord { assert(isPresent); pendingResult = result; currentState = _RouteLifecycle.pop; - route.onPopInvoked(true); + route.onPopInvoked(true, result); } bool _reportRemovalToObserver = true; @@ -5239,7 +5239,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res pop(result); return true; case RoutePopDisposition.doNotPop: - lastEntry.route.onPopInvoked(false); + lastEntry.route.onPopInvoked(false, result); return true; } } @@ -5282,7 +5282,7 @@ class NavigatorState extends State with TickerProviderStateMixin, Res assert(entry.route._popCompleter.isCompleted); entry.currentState = _RouteLifecycle.pop; } - entry.route.onPopInvoked(true); + entry.route.onPopInvoked(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 203a85bede..627a9cf53c 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) { + onPopInvoked: (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 c8b31f60c8..675f701c88 100644 --- a/packages/flutter/lib/src/widgets/pop_scope.dart +++ b/packages/flutter/lib/src/widgets/pop_scope.dart @@ -10,10 +10,15 @@ import 'routes.dart'; /// Manages back navigation gestures. /// +/// The generic type should match or be supertype of the generic type of the +/// enclosing [Route]. 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. +/// `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. @@ -41,6 +46,13 @@ 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 @@ -49,7 +61,7 @@ import 'routes.dart'; /// 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 { +class PopScope extends StatefulWidget { /// Creates a widget that registers a callback to veto attempts by the user to /// dismiss the enclosing [ModalRoute]. const PopScope({ @@ -78,10 +90,12 @@ 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. - final PopInvokedCallback? onPopInvoked; + final PopInvokedCallback? onPopInvoked; /// {@template flutter.widgets.PopScope.canPop} /// When false, blocks the current route from being popped. @@ -99,14 +113,16 @@ 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, T? result) { + widget.onPopInvoked?.call(didPop, result); + } @override late final ValueNotifier canPopNotifier; @@ -129,7 +145,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 277a5fb6c5..eb1976c5d6 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 @@ -1724,7 +1726,7 @@ 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.onPopInvoked(didPop, result); } } @@ -1793,7 +1795,7 @@ 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,7 +2415,9 @@ 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 PopInvokedCallback = void Function(bool didPop, T? result); /// Allows listening to and preventing pops. /// @@ -2425,9 +2429,13 @@ 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 { +abstract class PopEntry { /// {@macro flutter.widgets.PopScope.onPopInvoked} - PopInvokedCallback? get onPopInvoked; + // This can't be a function getter since dart vm doesn't allow upcasting + // generic type of the function getter. This prevents customers from declaring + // PopScope with any generic type that is subtype of ModalRoute._popEntries. + // See https://github.com/dart-lang/sdk/issues/55427. + void onPopInvoked(bool didPop, T? result); /// {@macro flutter.widgets.PopScope.canPop} ValueListenable get canPopNotifier; diff --git a/packages/flutter/test/cupertino/tab_test.dart b/packages/flutter/test/cupertino/tab_test.dart index 5acf173cd6..7188777509 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 0bb9ba9d5d..4cc9a7f49b 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) { + onPopInvoked: (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) { + onPopInvoked: (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) { + onPopInvoked: (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 PopInvokedCallback? onPopInvoked; @override Widget build(BuildContext context) { @@ -5575,7 +5575,7 @@ class _LinksPage extends StatelessWidget { child: const Text('Go back'), ), if (canPop != null) - PopScope( + PopScope( canPop: canPop!, onPopInvoked: 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 116951ce78..7da2019ddd 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, + onPopInvoked: (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, + onPopInvoked: (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, + onPopInvoked: (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) { + onPopInvoked: (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'), ),