Skip to content

Commit a36ff80

Browse files
authored
Refactors page API (#137792)
fixes flutter/flutter#137458 Chagnes: 1. Navigator.pop will always pop page based route 2. add a onDidRemovePage callback to replace onPopPage 3. Page.canPop and Page.onPopInvoked mirrors the PopScope, but in Page class. migration guide flutter/website#10523
1 parent 27f683d commit a36ff80

File tree

8 files changed

+358
-76
lines changed

8 files changed

+358
-76
lines changed

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,10 @@ class _BottomNavTabState extends State<_BottomNavTab> {
168168
},
169169
child: Navigator(
170170
key: _navigatorKey,
171-
onPopPage: (Route<void> route, void result) {
172-
if (!route.didPop(null)) {
173-
return false;
174-
}
171+
onDidRemovePage: (Page<Object?> page) {
175172
widget.onChangedPages(<_TabPage>[
176173
...widget.pages,
177174
]..removeLast());
178-
return true;
179175
},
180176
pages: widget.pages.map((_TabPage page) {
181177
switch (page) {
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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 a confirmation dialog before navigating
6+
// away from a page.
7+
8+
import 'package:flutter/material.dart';
9+
10+
void main() => runApp(const PageApiExampleApp());
11+
12+
class PageApiExampleApp extends StatefulWidget {
13+
const PageApiExampleApp({super.key});
14+
15+
@override
16+
State<PageApiExampleApp> createState() => _PageApiExampleAppState();
17+
}
18+
19+
class _PageApiExampleAppState extends State<PageApiExampleApp> {
20+
final RouterDelegate<Object> delegate = MyRouterDelegate();
21+
22+
@override
23+
Widget build(BuildContext context) {
24+
return MaterialApp.router(
25+
routerDelegate: delegate,
26+
);
27+
}
28+
}
29+
30+
class MyRouterDelegate extends RouterDelegate<Object> with PopNavigatorRouterDelegateMixin<Object>, ChangeNotifier {
31+
// This example doesn't use RouteInformationProvider.
32+
@override
33+
Future<void> setNewRoutePath(Object configuration) async => throw UnimplementedError();
34+
35+
@override
36+
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
37+
38+
static MyRouterDelegate of(BuildContext context) => Router.of(context).routerDelegate as MyRouterDelegate;
39+
40+
bool get showDetailPage => _showDetailPage;
41+
bool _showDetailPage = false;
42+
set showDetailPage(bool value) {
43+
if (_showDetailPage == value) {
44+
return;
45+
}
46+
_showDetailPage = value;
47+
notifyListeners();
48+
}
49+
50+
Future<bool> _showConfirmDialog() async {
51+
return await showDialog<bool>(
52+
context: navigatorKey.currentContext!,
53+
barrierDismissible: false,
54+
builder: (BuildContext context) {
55+
return AlertDialog(
56+
title: const Text('Are you sure?'),
57+
actions: <Widget>[
58+
TextButton(
59+
child: const Text('Cancel'),
60+
onPressed: () {
61+
Navigator.of(context).pop(false);
62+
},
63+
),
64+
TextButton(
65+
child: const Text('Confirm'),
66+
onPressed: () {
67+
Navigator.of(context).pop(true);
68+
},
69+
),
70+
],
71+
);
72+
},
73+
) ?? false;
74+
}
75+
76+
Future<void> _handlePopDetails(bool didPop, void result) async {
77+
if (didPop) {
78+
showDetailPage = false;
79+
return;
80+
}
81+
final bool confirmed = await _showConfirmDialog();
82+
if (confirmed) {
83+
showDetailPage = false;
84+
}
85+
}
86+
87+
List<Page<Object?>> _getPages() {
88+
return <Page<Object?>>[
89+
const MaterialPage<void>(key: ValueKey<String>('home'), child: _HomePage()),
90+
if (showDetailPage)
91+
MaterialPage<void>(
92+
key: const ValueKey<String>('details'),
93+
child: const _DetailsPage(),
94+
canPop: false,
95+
onPopInvoked: _handlePopDetails,
96+
),
97+
];
98+
}
99+
100+
@override
101+
Widget build(BuildContext context) {
102+
return Navigator(
103+
key: navigatorKey,
104+
pages: _getPages(),
105+
onDidRemovePage: (Page<Object?> page) {
106+
assert(page.key == const ValueKey<String>('details'));
107+
showDetailPage = false;
108+
},
109+
);
110+
}
111+
}
112+
113+
class _HomePage extends StatefulWidget {
114+
const _HomePage();
115+
116+
@override
117+
State<_HomePage> createState() => _HomePageState();
118+
}
119+
120+
class _HomePageState extends State<_HomePage> {
121+
122+
@override
123+
Widget build(BuildContext context) {
124+
return Scaffold(
125+
appBar: AppBar(title: const Text('Home')),
126+
body: Center(
127+
child: TextButton(
128+
onPressed: () {
129+
MyRouterDelegate.of(context).showDetailPage = true;
130+
},
131+
child: const Text('Go to details'),
132+
),
133+
),
134+
);
135+
}
136+
}
137+
138+
class _DetailsPage extends StatefulWidget {
139+
const _DetailsPage();
140+
141+
@override
142+
State<_DetailsPage> createState() => _DetailsPageState();
143+
}
144+
145+
class _DetailsPageState extends State<_DetailsPage> {
146+
@override
147+
Widget build(BuildContext context) {
148+
return Scaffold(
149+
appBar: AppBar(title: const Text('Details')),
150+
body: Center(
151+
child: TextButton(
152+
onPressed: () {
153+
Navigator.of(context).maybePop();
154+
},
155+
child: const Text('Go back'),
156+
),
157+
),
158+
);
159+
}
160+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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_api_samples/widgets/page/page_can_pop.0.dart' as example;
6+
import 'package:flutter_test/flutter_test.dart';
7+
8+
import '../navigator_utils.dart';
9+
10+
void main() {
11+
testWidgets('Can choose to stay on page', (WidgetTester tester) async {
12+
await tester.pumpWidget(
13+
const example.PageApiExampleApp(),
14+
);
15+
16+
expect(find.text('Home'), findsOneWidget);
17+
18+
await tester.tap(find.text('Go to details'));
19+
await tester.pumpAndSettle();
20+
expect(find.text('Home'), findsNothing);
21+
expect(find.text('Details'), findsOneWidget);
22+
23+
await simulateSystemBack();
24+
await tester.pumpAndSettle();
25+
expect(find.text('Are you sure?'), findsOneWidget);
26+
27+
await tester.tap(find.text('Cancel'));
28+
await tester.pumpAndSettle();
29+
expect(find.text('Home'), findsNothing);
30+
expect(find.text('Details'), findsOneWidget);
31+
});
32+
33+
testWidgets('Can choose to go back', (WidgetTester tester) async {
34+
await tester.pumpWidget(
35+
const example.PageApiExampleApp(),
36+
);
37+
38+
expect(find.text('Home'), findsOneWidget);
39+
40+
await tester.tap(find.text('Go to details'));
41+
await tester.pumpAndSettle();
42+
expect(find.text('Home'), findsNothing);
43+
expect(find.text('Details'), findsOneWidget);
44+
45+
await simulateSystemBack();
46+
await tester.pumpAndSettle();
47+
expect(find.text('Are you sure?'), findsOneWidget);
48+
49+
await tester.tap(find.text('Confirm'));
50+
await tester.pumpAndSettle();
51+
expect(find.text('Details'), findsNothing);
52+
expect(find.text('Home'), findsOneWidget);
53+
});
54+
}

packages/flutter/lib/src/cupertino/route.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,8 @@ class CupertinoPage<T> extends Page<T> {
347347
this.title,
348348
this.fullscreenDialog = false,
349349
this.allowSnapshotting = true,
350+
super.canPop,
351+
super.onPopInvoked,
350352
super.key,
351353
super.name,
352354
super.arguments,

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ class MaterialPage<T> extends Page<T> {
150150
this.fullscreenDialog = false,
151151
this.allowSnapshotting = true,
152152
super.key,
153+
super.canPop,
154+
super.onPopInvoked,
153155
super.name,
154156
super.arguments,
155157
super.restorationId,

0 commit comments

Comments
 (0)