Add generic type for result in PopScope (#139164)
Adds a generic type and pop result to popscope and its friend.
The use cases are to be able to capture the result when the pop is called.
migration guide: https://github.com/flutter/website/pull/9872
diff --git a/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart b/dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart
index a68c2a1..a063c7c 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 @@
@override
Widget build(BuildContext context) {
- return PopScope(
+ return PopScope<Object?>(
// 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 81ee4da..86c5773 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 @@
bool _hasName = false;
late String _eventName;
- Future<void> _handlePopInvoked(bool didPop) async {
+ Future<void> _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 c6f644e..dd479d5 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 @@
return null;
}
- Future<void> _handlePopInvoked(bool didPop) async {
+ Future<void> _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 5e7a951..1a90c14 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 @@
// 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 @@
duration: const Duration(milliseconds: 225),
curve: Curves.easeInOut,
alignment: FractionalOffset.topLeft,
- child: PopScope(
+ child: PopScope<Object?>(
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 a6ceab8..1e57e39 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 @@
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
body: SafeArea(
bottom: false,
- child: PopScope(
+ child: PopScope<Object?>(
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 e008f5a..c6d602d 100644
--- a/examples/api/lib/widgets/form/form.1.dart
+++ b/examples/api/lib/widgets/form/form.1.dart
@@ -111,7 +111,7 @@
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 2400b09..c86640c 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 @@
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text('Page Two'),
- PopScope(
+ PopScope<Object?>(
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 0000000..cb3b9f2
--- /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<FormData>(
+ builder: (BuildContext context) => const _PageTwo(),
+ ),
+ _ => MaterialPageRoute<void>(
+ 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: <Widget>[
+ 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<FormData?>('/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<bool?> _showBackDialog(BuildContext context) {
+ return showDialog<bool>(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: const Text('Are you sure?'),
+ content: const Text(
+ 'Are you sure you want to leave this page?',
+ ),
+ actions: <Widget>[
+ TextButton(
+ style: TextButton.styleFrom(
+ textStyle: Theme.of(context).textTheme.labelLarge,
+ ),
+ child: const Text('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<FormData>(
+ 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: <Widget>[
+ const Text('Page Two'),
+ Form(
+ child: Column(
+ children: <Widget>[
+ 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 0000000..14266af
--- /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 a61f3a0..308a9d8 100644
--- a/packages/flutter/lib/src/material/about.dart
+++ b/packages/flutter/lib/src/material/about.dart
@@ -1230,9 +1230,9 @@
}
MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
- return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
- return PopScope(
- onPopInvoked: (bool didPop) {
+ return MaterialPageRoute<void>(builder: (BuildContext context) {
+ return PopScope<void>(
+ 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 2211523..823235b 100644
--- a/packages/flutter/lib/src/widgets/form.dart
+++ b/packages/flutter/lib/src/widgets/form.dart
@@ -177,7 +177,7 @@
/// * [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<Object?>? onPopInvoked;
/// Called when one of the form fields changes.
///
@@ -244,7 +244,7 @@
}
if (widget.canPop != null || widget.onPopInvoked != null) {
- return PopScope(
+ return PopScope<Object?>(
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 88b869b..edf3eb1 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -355,7 +355,7 @@
/// 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 @@
assert(isPresent);
pendingResult = result;
currentState = _RouteLifecycle.pop;
- route.onPopInvoked(true);
+ route.onPopInvoked(true, result);
}
bool _reportRemovalToObserver = true;
@@ -5239,7 +5239,7 @@
pop(result);
return true;
case RoutePopDisposition.doNotPop:
- lastEntry.route.onPopInvoked(false);
+ lastEntry.route.onPopInvoked(false, result);
return true;
}
}
@@ -5282,7 +5282,7 @@
assert(entry.route._popCompleter.isCompleted);
entry.currentState = _RouteLifecycle.pop;
}
- entry.route.onPopInvoked(true);
+ entry.route.onPopInvoked(true, result);
} else {
entry.pop<T>(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 203a85b..627a9cf 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 @@
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<Object?>(
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 c8b31f6..675f701 100644
--- a/packages/flutter/lib/src/widgets/pop_scope.dart
+++ b/packages/flutter/lib/src/widgets/pop_scope.dart
@@ -10,10 +10,15 @@
/// 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<int>`,
+/// 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 @@
/// ** 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 @@
/// 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<T> 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 @@
/// 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<T>? onPopInvoked;
/// {@template flutter.widgets.PopScope.canPop}
/// When false, blocks the current route from being popped.
@@ -99,14 +113,16 @@
final bool canPop;
@override
- State<PopScope> createState() => _PopScopeState();
+ State<PopScope<T>> createState() => _PopScopeState<T>();
}
-class _PopScopeState extends State<PopScope> implements PopEntry {
+class _PopScopeState<T> extends State<PopScope<T>> implements PopEntry<T> {
ModalRoute<dynamic>? _route;
@override
- PopInvokedCallback? get onPopInvoked => widget.onPopInvoked;
+ void onPopInvoked(bool didPop, T? result) {
+ widget.onPopInvoked?.call(didPop, result);
+ }
@override
late final ValueNotifier<bool> canPopNotifier;
@@ -129,7 +145,7 @@
}
@override
- void didUpdateWidget(PopScope oldWidget) {
+ void didUpdateWidget(PopScope<T> 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 277a5fb..eb1976c 100644
--- a/packages/flutter/lib/src/widgets/routes.dart
+++ b/packages/flutter/lib/src/widgets/routes.dart
@@ -1669,7 +1669,9 @@
final List<WillPopCallback> _willPopCallbacks = <WillPopCallback>[];
- final Set<PopEntry> _popEntries = <PopEntry>{};
+ // Holding as Object? instead of T so that PopScope in this route can be
+ // declared with any supertype of T.
+ final Set<PopEntry<Object?>> _popEntries = <PopEntry<Object?>>{};
/// Returns [RoutePopDisposition.doNotPop] if any of callbacks added with
/// [addScopedWillPopCallback] returns either false or null. If they all
@@ -1724,7 +1726,7 @@
/// method checks.
@override
RoutePopDisposition get popDisposition {
- for (final PopEntry popEntry in _popEntries) {
+ for (final PopEntry<Object?> popEntry in _popEntries) {
if (!popEntry.canPopNotifier.value) {
return RoutePopDisposition.doNotPop;
}
@@ -1734,9 +1736,9 @@
}
@override
- void onPopInvoked(bool didPop) {
- for (final PopEntry popEntry in _popEntries) {
- popEntry.onPopInvoked?.call(didPop);
+ void onPopInvoked(bool didPop, T? result) {
+ for (final PopEntry<Object?> popEntry in _popEntries) {
+ popEntry.onPopInvoked(didPop, result);
}
}
@@ -1793,7 +1795,7 @@
/// See also:
///
/// * [unregisterPopEntry], which performs the opposite operation.
- void registerPopEntry(PopEntry popEntry) {
+ void registerPopEntry(PopEntry<Object?> popEntry) {
_popEntries.add(popEntry);
popEntry.canPopNotifier.addListener(_handlePopEntryChange);
_handlePopEntryChange();
@@ -1804,7 +1806,7 @@
/// See also:
///
/// * [registerPopEntry], which performs the opposite operation.
- void unregisterPopEntry(PopEntry popEntry) {
+ void unregisterPopEntry(PopEntry<Object?> popEntry) {
_popEntries.remove(popEntry);
popEntry.canPopNotifier.removeListener(_handlePopEntryChange);
_handlePopEntryChange();
@@ -2413,7 +2415,9 @@
///
/// 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<T> = void Function(bool didPop, T? result);
/// Allows listening to and preventing pops.
///
@@ -2425,9 +2429,13 @@
/// * [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<T> {
/// {@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<bool> get canPopNotifier;
diff --git a/packages/flutter/test/cupertino/tab_test.dart b/packages/flutter/test/cupertino/tab_test.dart
index 5acf173..71887775 100644
--- a/packages/flutter/test/cupertino/tab_test.dart
+++ b/packages/flutter/test/cupertino/tab_test.dart
@@ -305,7 +305,7 @@
BottomNavigationBarItem(label: '', icon: Text('2'))
],
),
- tabBuilder: (_, int i) => PopScope(
+ tabBuilder: (_, int i) => PopScope<Object?>(
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 0bb9ba9..4cc9a7f 100644
--- a/packages/flutter/test/widgets/navigator_test.dart
+++ b/packages/flutter/test/widgets/navigator_test.dart
@@ -2989,7 +2989,7 @@
const List<Page<void>> myPages = <Page<void>>[
MaterialPage<void>(child: Text('page1')),
MaterialPage<void>(
- child: PopScope(
+ child: PopScope<void>(
canPop: false,
child: Text('page2'),
),
@@ -4908,9 +4908,9 @@
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
builderSetState = setState;
- return PopScope(
+ return PopScope<Object?>(
canPop: canPop(),
- onPopInvoked: (bool success) {
+ onPopInvoked: (bool success, Object? result) {
if (success || pages.last == _Page.noPop) {
return;
}
@@ -5024,9 +5024,9 @@
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
- return PopScope(
+ return PopScope<Object?>(
canPop: canPop(),
- onPopInvoked: (bool success) {
+ onPopInvoked: (bool success, Object? result) {
if (success || pages.last == _Page.noPop) {
return;
}
@@ -5117,9 +5117,9 @@
MaterialApp(
home: StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
- return PopScope(
+ return PopScope<Object?>(
canPop: canPop(),
- onPopInvoked: (bool success) {
+ onPopInvoked: (bool success, Object? result) {
if (success || pages.last == _PageWithYesPop.noPop) {
return;
}
@@ -5189,7 +5189,7 @@
child: _LinksPage(
title: 'Can pop page',
canPop: true,
- onPopInvoked: (bool didPop) {
+ onPopInvoked: (bool didPop, void result) {
onPopInvokedCallCount += 1;
},
),
@@ -5556,7 +5556,7 @@
final bool? canPop;
final VoidCallback? onBack;
final String title;
- final PopInvokedCallback? onPopInvoked;
+ final PopInvokedCallback<Object?>? onPopInvoked;
@override
Widget build(BuildContext context) {
@@ -5575,7 +5575,7 @@
child: const Text('Go back'),
),
if (canPop != null)
- PopScope(
+ PopScope<void>(
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 116951c..7da2019 100644
--- a/packages/flutter/test/widgets/pop_scope_test.dart
+++ b/packages/flutter/test/widgets/pop_scope_test.dart
@@ -47,7 +47,7 @@
builder: (BuildContext buildContext, StateSetter stateSetter) {
context = buildContext;
setState = stateSetter;
- return PopScope(
+ return PopScope<Object?>(
canPop: canPop,
child: const Center(
child: Column(
@@ -79,6 +79,94 @@
variant: TargetPlatformVariant.all(),
);
+ testWidgets('pop scope can receive result', (WidgetTester tester) async {
+ Object? receivedResult;
+ final Object poppedResult = Object();
+ final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>();
+ await tester.pumpWidget(
+ MaterialApp(
+ initialRoute: '/',
+ navigatorKey: nav,
+ home: Scaffold(
+ body: PopScope<Object?>(
+ canPop: false,
+ onPopInvoked: (bool didPop, Object? result) {
+ receivedResult = result;
+ },
+ child: const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ 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<NavigatorState> nav = GlobalKey<NavigatorState>();
+ await tester.pumpWidget(
+ MaterialApp(
+ initialRoute: '/',
+ navigatorKey: nav,
+ home: Scaffold(
+ body: PopScope<Object?>(
+ canPop: false,
+ onPopInvoked: (bool didPop, Object? result) {
+ receivedResult = result;
+ },
+ child: const Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: <Widget>[
+ Text('Home/PopScope Page'),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+
+ nav.currentState!.push(
+ MaterialPageRoute<int>(
+ builder: (BuildContext context) {
+ return Scaffold(
+ body: PopScope<Object?>(
+ 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<NavigatorState> nav = GlobalKey<NavigatorState>();
bool canPop = true;
@@ -115,9 +203,9 @@
builder: (BuildContext context, StateSetter stateSetter) {
oneContext = context;
setState = stateSetter;
- return PopScope(
+ return PopScope<Object?>(
canPop: canPop,
- onPopInvoked: (bool didPop) {
+ onPopInvoked: (bool didPop, Object? result) {
lastPopSuccess = didPop;
},
child: const Center(
@@ -271,7 +359,7 @@
if (!usePopScope) {
return child;
}
- return const PopScope(
+ return const PopScope<Object?>(
canPop: false,
child: child,
);
@@ -314,12 +402,12 @@
return Column(
children: <Widget>[
if (usePopScope1)
- const PopScope(
+ const PopScope<Object?>(
canPop: false,
child: Text('hello'),
),
if (usePopScope2)
- const PopScope(
+ const PopScope<Object?>(
canPop: false,
child: Text('hello'),
),