Skip to content

Commit 7ee05a0

Browse files
authored
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: flutter/website#9872
1 parent ffa3b30 commit 7ee05a0

File tree

18 files changed

+466
-55
lines changed

18 files changed

+466
-55
lines changed

dev/integration_tests/flutter_gallery/lib/demo/cupertino/cupertino_navigation_demo.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ class CupertinoNavigationDemo extends StatelessWidget {
4848

4949
@override
5050
Widget build(BuildContext context) {
51-
return PopScope(
51+
return PopScope<Object?>(
5252
// Prevent swipe popping of this page. Use explicit exit buttons only.
5353
canPop: false,
5454
child: DefaultTextStyle(

dev/integration_tests/flutter_gallery/lib/demo/material/full_screen_dialog_demo.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ class FullScreenDialogDemoState extends State<FullScreenDialogDemo> {
110110
bool _hasName = false;
111111
late String _eventName;
112112

113-
Future<void> _handlePopInvoked(bool didPop) async {
113+
Future<void> _handlePopInvoked(bool didPop, Object? result) async {
114114
if (didPop) {
115115
return;
116116
}

dev/integration_tests/flutter_gallery/lib/demo/material/text_form_field_demo.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ class TextFormFieldDemoState extends State<TextFormFieldDemo> {
143143
return null;
144144
}
145145

146-
Future<void> _handlePopInvoked(bool didPop) async {
146+
Future<void> _handlePopInvoked(bool didPop, Object? result) async {
147147
if (didPop) {
148148
return;
149149
}

dev/integration_tests/flutter_gallery/lib/demo/shrine/expanding_bottom_sheet.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP
355355

356356
// Closes the cart if the cart is open, otherwise exits the app (this should
357357
// only be relevant for Android).
358-
void _handlePopInvoked(bool didPop) {
358+
void _handlePopInvoked(bool didPop, Object? result) {
359359
if (didPop) {
360360
return;
361361
}
@@ -370,7 +370,7 @@ class ExpandingBottomSheetState extends State<ExpandingBottomSheet> with TickerP
370370
duration: const Duration(milliseconds: 225),
371371
curve: Curves.easeInOut,
372372
alignment: FractionalOffset.topLeft,
373-
child: PopScope(
373+
child: PopScope<Object?>(
374374
canPop: !_isOpen,
375375
onPopInvoked: _handlePopInvoked,
376376
child: AnimatedBuilder(

dev/integration_tests/flutter_gallery/lib/gallery/home.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -326,9 +326,9 @@ class _GalleryHomeState extends State<GalleryHome> with SingleTickerProviderStat
326326
backgroundColor: isDark ? _kFlutterBlue : theme.primaryColor,
327327
body: SafeArea(
328328
bottom: false,
329-
child: PopScope(
329+
child: PopScope<Object?>(
330330
canPop: _category == null,
331-
onPopInvoked: (bool didPop) {
331+
onPopInvoked: (bool didPop, Object? result) {
332332
if (didPop) {
333333
return;
334334
}

examples/api/lib/widgets/form/form.1.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ class _SaveableFormState extends State<_SaveableForm> {
111111
const SizedBox(height: 20.0),
112112
Form(
113113
canPop: !_isDirty,
114-
onPopInvoked: (bool didPop) async {
114+
onPopInvoked: (bool didPop, Object? result) async {
115115
if (didPop) {
116116
return;
117117
}

examples/api/lib/widgets/pop_scope/pop_scope.0.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,9 @@ class _PageTwoState extends State<_PageTwo> {
109109
mainAxisAlignment: MainAxisAlignment.center,
110110
children: <Widget>[
111111
const Text('Page Two'),
112-
PopScope(
112+
PopScope<Object?>(
113113
canPop: false,
114-
onPopInvoked: (bool didPop) async {
114+
onPopInvoked: (bool didPop, Object? result) async {
115115
if (didPop) {
116116
return;
117117
}
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
// This sample demonstrates showing how to use PopScope to wrap widget that
6+
// may pop the page with a result.
7+
8+
import 'package:flutter/material.dart';
9+
10+
void main() => runApp(const NavigatorPopHandlerApp());
11+
12+
class NavigatorPopHandlerApp extends StatelessWidget {
13+
const NavigatorPopHandlerApp({super.key});
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return MaterialApp(
18+
initialRoute: '/home',
19+
onGenerateRoute: (RouteSettings settings) {
20+
return switch (settings.name) {
21+
'/two' => MaterialPageRoute<FormData>(
22+
builder: (BuildContext context) => const _PageTwo(),
23+
),
24+
_ => MaterialPageRoute<void>(
25+
builder: (BuildContext context) => const _HomePage(),
26+
),
27+
};
28+
},
29+
);
30+
}
31+
}
32+
33+
class _HomePage extends StatefulWidget {
34+
const _HomePage();
35+
36+
@override
37+
State<_HomePage> createState() => _HomePageState();
38+
}
39+
40+
class _HomePageState extends State<_HomePage> {
41+
FormData? _formData;
42+
43+
@override
44+
Widget build(BuildContext context) {
45+
return Scaffold(
46+
body: Center(
47+
child: Column(
48+
mainAxisAlignment: MainAxisAlignment.center,
49+
children: <Widget>[
50+
const Text('Page One'),
51+
if (_formData != null)
52+
Text('Hello ${_formData!.name}, whose favorite food is ${_formData!.favoriteFood}.'),
53+
TextButton(
54+
onPressed: () async {
55+
final FormData formData =
56+
await Navigator.of(context).pushNamed<FormData?>('/two')
57+
?? const FormData();
58+
if (formData != _formData) {
59+
setState(() {
60+
_formData = formData;
61+
});
62+
}
63+
},
64+
child: const Text('Next page'),
65+
),
66+
],
67+
),
68+
),
69+
);
70+
}
71+
}
72+
73+
class _PopScopeWrapper extends StatelessWidget {
74+
const _PopScopeWrapper({required this.child});
75+
76+
final Widget child;
77+
78+
Future<bool?> _showBackDialog(BuildContext context) {
79+
return showDialog<bool>(
80+
context: context,
81+
builder: (BuildContext context) {
82+
return AlertDialog(
83+
title: const Text('Are you sure?'),
84+
content: const Text(
85+
'Are you sure you want to leave this page?',
86+
),
87+
actions: <Widget>[
88+
TextButton(
89+
style: TextButton.styleFrom(
90+
textStyle: Theme.of(context).textTheme.labelLarge,
91+
),
92+
child: const Text('Never mind'),
93+
onPressed: () {
94+
Navigator.pop(context, false);
95+
},
96+
),
97+
TextButton(
98+
style: TextButton.styleFrom(
99+
textStyle: Theme.of(context).textTheme.labelLarge,
100+
),
101+
child: const Text('Leave'),
102+
onPressed: () {
103+
Navigator.pop(context, true);
104+
},
105+
),
106+
],
107+
);
108+
},
109+
);
110+
}
111+
@override
112+
Widget build(BuildContext context) {
113+
return PopScope<FormData>(
114+
canPop: false,
115+
// The result contains pop result in `_PageTwo`.
116+
onPopInvoked: (bool didPop, FormData? result) async {
117+
if (didPop) {
118+
return;
119+
}
120+
final bool shouldPop = await _showBackDialog(context) ?? false;
121+
if (context.mounted && shouldPop) {
122+
Navigator.pop(context, result);
123+
}
124+
},
125+
child: const _PageTwoBody(),
126+
);
127+
}
128+
}
129+
130+
// This is a PopScope wrapper over _PageTwoBody
131+
class _PageTwo extends StatelessWidget {
132+
const _PageTwo();
133+
134+
@override
135+
Widget build(BuildContext context) {
136+
return const _PopScopeWrapper(
137+
child: _PageTwoBody(),
138+
);
139+
}
140+
141+
}
142+
143+
class _PageTwoBody extends StatefulWidget {
144+
const _PageTwoBody();
145+
146+
@override
147+
State<_PageTwoBody> createState() => _PageTwoBodyState();
148+
}
149+
150+
class _PageTwoBodyState extends State<_PageTwoBody> {
151+
FormData _formData = const FormData();
152+
153+
@override
154+
Widget build(BuildContext context) {
155+
return Scaffold(
156+
body: Center(
157+
child: Column(
158+
mainAxisAlignment: MainAxisAlignment.center,
159+
children: <Widget>[
160+
const Text('Page Two'),
161+
Form(
162+
child: Column(
163+
children: <Widget>[
164+
TextFormField(
165+
decoration: const InputDecoration(
166+
hintText: 'Enter your name.',
167+
),
168+
onChanged: (String value) {
169+
_formData = _formData.copyWith(
170+
name: value,
171+
);
172+
},
173+
),
174+
TextFormField(
175+
decoration: const InputDecoration(
176+
hintText: 'Enter your favorite food.',
177+
),
178+
onChanged: (String value) {
179+
_formData = _formData.copyWith(
180+
favoriteFood: value,
181+
);
182+
},
183+
),
184+
],
185+
),
186+
),
187+
TextButton(
188+
onPressed: () async {
189+
Navigator.maybePop(context, _formData);
190+
},
191+
child: const Text('Go back'),
192+
),
193+
],
194+
),
195+
),
196+
);
197+
}
198+
}
199+
200+
@immutable
201+
class FormData {
202+
const FormData({
203+
this.name = '',
204+
this.favoriteFood = '',
205+
});
206+
207+
final String name;
208+
final String favoriteFood;
209+
210+
FormData copyWith({String? name, String? favoriteFood}) {
211+
return FormData(
212+
name: name ?? this.name,
213+
favoriteFood: favoriteFood ?? this.favoriteFood,
214+
);
215+
}
216+
217+
@override
218+
bool operator ==(Object other) {
219+
if (identical(this, other)) {
220+
return true;
221+
}
222+
if (other.runtimeType != runtimeType) {
223+
return false;
224+
}
225+
return other is FormData
226+
&& other.name == name
227+
&& other.favoriteFood == favoriteFood;
228+
}
229+
230+
@override
231+
int get hashCode => Object.hash(name, favoriteFood);
232+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/widgets/pop_scope/pop_scope.1.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
import '../navigator_utils.dart';
10+
11+
void main() {
12+
testWidgets('Can choose to stay on page', (WidgetTester tester) async {
13+
await tester.pumpWidget(
14+
const example.NavigatorPopHandlerApp(),
15+
);
16+
17+
expect(find.text('Page One'), findsOneWidget);
18+
19+
await tester.tap(find.text('Next page'));
20+
await tester.pumpAndSettle();
21+
expect(find.text('Page One'), findsNothing);
22+
expect(find.text('Page Two'), findsOneWidget);
23+
24+
await simulateSystemBack();
25+
await tester.pumpAndSettle();
26+
expect(find.text('Page One'), findsNothing);
27+
expect(find.text('Page Two'), findsOneWidget);
28+
expect(find.text('Are you sure?'), findsOneWidget);
29+
30+
await tester.tap(find.text('Never mind'));
31+
await tester.pumpAndSettle();
32+
expect(find.text('Page One'), findsNothing);
33+
expect(find.text('Page Two'), findsOneWidget);
34+
});
35+
36+
testWidgets('Can choose to go back with pop result', (WidgetTester tester) async {
37+
await tester.pumpWidget(
38+
const example.NavigatorPopHandlerApp(),
39+
);
40+
41+
expect(find.text('Page One'), findsOneWidget);
42+
expect(find.text('Page Two'), findsNothing);
43+
44+
await tester.tap(find.text('Next page'));
45+
await tester.pumpAndSettle();
46+
expect(find.text('Page One'), findsNothing);
47+
expect(find.text('Page Two'), findsOneWidget);
48+
49+
await tester.enterText(find.byType(TextFormField).first, 'John');
50+
await tester.pumpAndSettle();
51+
await tester.enterText(find.byType(TextFormField).last, 'Apple');
52+
await tester.pumpAndSettle();
53+
54+
await tester.tap(find.text('Go back'));
55+
await tester.pumpAndSettle();
56+
expect(find.text('Page One'), findsNothing);
57+
expect(find.text('Page Two'), findsOneWidget);
58+
expect(find.text('Are you sure?'), findsOneWidget);
59+
60+
await tester.tap(find.text('Leave'));
61+
await tester.pumpAndSettle();
62+
expect(find.text('Page One'), findsOneWidget);
63+
expect(find.text('Page Two'), findsNothing);
64+
expect(find.text('Are you sure?'), findsNothing);
65+
expect(find.text('Hello John, whose favorite food is Apple.'), findsOneWidget);
66+
});
67+
}

packages/flutter/lib/src/material/about.dart

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1230,9 +1230,9 @@ class _MasterDetailFlowState extends State<_MasterDetailFlow> implements _PageOp
12301230
}
12311231

12321232
MaterialPageRoute<void> _detailPageRoute(Object? arguments) {
1233-
return MaterialPageRoute<dynamic>(builder: (BuildContext context) {
1234-
return PopScope(
1235-
onPopInvoked: (bool didPop) {
1233+
return MaterialPageRoute<void>(builder: (BuildContext context) {
1234+
return PopScope<void>(
1235+
onPopInvoked: (bool didPop, void result) {
12361236
// No need for setState() as rebuild happens on navigation pop.
12371237
focus = _Focus.master;
12381238
},

0 commit comments

Comments
 (0)