Skip to content

Commit 01c1e8e

Browse files
authored
Allows pushing page based route as pageless route (#114362)
* Allows pushing page based route as pageless route * update
1 parent 22e1ac7 commit 01c1e8e

File tree

2 files changed

+52
-108
lines changed

2 files changed

+52
-108
lines changed

packages/flutter/lib/src/widgets/navigator.dart

+32-52
Original file line numberDiff line numberDiff line change
@@ -2806,8 +2806,10 @@ class _RouteEntry extends RouteTransitionRecord {
28062806
_RouteEntry(
28072807
this.route, {
28082808
required _RouteLifecycle initialState,
2809+
required this.pageBased,
28092810
this.restorationInformation,
28102811
}) : assert(route != null),
2812+
assert(!pageBased || route.settings is Page),
28112813
assert(initialState != null),
28122814
assert(
28132815
initialState == _RouteLifecycle.staging ||
@@ -2821,6 +2823,7 @@ class _RouteEntry extends RouteTransitionRecord {
28212823
@override
28222824
final Route<dynamic> route;
28232825
final _RestorationInformation? restorationInformation;
2826+
final bool pageBased;
28242827

28252828
static Route<dynamic> notAnnounced = _NotAnnounced();
28262829

@@ -2834,7 +2837,7 @@ class _RouteEntry extends RouteTransitionRecord {
28342837
String? get restorationId {
28352838
// User-provided restoration ids of Pages are prefixed with 'p+'. Generated
28362839
// ids for pageless routes are prefixed with 'r+' to avoid clashes.
2837-
if (hasPage) {
2840+
if (pageBased) {
28382841
final Page<Object?> page = route.settings as Page<Object?>;
28392842
return page.restorationId != null ? 'p+${page.restorationId}' : null;
28402843
}
@@ -2844,13 +2847,11 @@ class _RouteEntry extends RouteTransitionRecord {
28442847
return null;
28452848
}
28462849

2847-
bool get hasPage => route.settings is Page;
2848-
28492850
bool canUpdateFrom(Page<dynamic> page) {
28502851
if (!willBePresent) {
28512852
return false;
28522853
}
2853-
if (!hasPage) {
2854+
if (!pageBased) {
28542855
return false;
28552856
}
28562857
final Page<dynamic> routePage = route.settings as Page<dynamic>;
@@ -2937,7 +2938,7 @@ class _RouteEntry extends RouteTransitionRecord {
29372938
if (route._popCompleter.isCompleted) {
29382939
// This is a page-based route popped through the Navigator.pop. The
29392940
// didPop should have been called. No further action is needed.
2940-
assert(hasPage);
2941+
assert(pageBased);
29412942
assert(pendingResult == null);
29422943
return true;
29432944
}
@@ -2989,7 +2990,7 @@ class _RouteEntry extends RouteTransitionRecord {
29892990
// Route is removed without being completed.
29902991
void remove({ bool isReplaced = false }) {
29912992
assert(
2992-
!hasPage || isWaitingForExitingDecision,
2993+
!pageBased || isWaitingForExitingDecision,
29932994
'A page-based route cannot be completed using imperative api, provide a '
29942995
'new list without the corresponding Page to Navigator.pages instead. ',
29952996
);
@@ -3004,7 +3005,7 @@ class _RouteEntry extends RouteTransitionRecord {
30043005
// Route completes with `result` and is removed.
30053006
void complete<T>(T result, { bool isReplaced = false }) {
30063007
assert(
3007-
!hasPage || isWaitingForExitingDecision,
3008+
!pageBased || isWaitingForExitingDecision,
30083009
'A page-based route cannot be completed using imperative api, provide a '
30093010
'new list without the corresponding Page to Navigator.pages instead. ',
30103011
);
@@ -3325,6 +3326,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
33253326
for (final Page<dynamic> page in widget.pages) {
33263327
final _RouteEntry entry = _RouteEntry(
33273328
page.createRoute(context),
3329+
pageBased: true,
33283330
initialState: _RouteLifecycle.add,
33293331
);
33303332
assert(
@@ -3349,6 +3351,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
33493351
widget.initialRoute ?? Navigator.defaultRouteName,
33503352
).map((Route<dynamic> route) => _RouteEntry(
33513353
route,
3354+
pageBased: false,
33523355
initialState: _RouteLifecycle.add,
33533356
restorationInformation: route.settings.name != null
33543357
? _RestorationInformation.named(
@@ -3652,7 +3655,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
36523655
assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
36533656
// Records pageless route. The bottom most pageless routes will be
36543657
// stored in key = null.
3655-
if (!oldEntry.hasPage) {
3658+
if (!oldEntry.pageBased) {
36563659
final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes.putIfAbsent(
36573660
previousOldPageRouteEntry,
36583661
() => <_RouteEntry>[],
@@ -3681,7 +3684,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
36813684
while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
36823685
final _RouteEntry oldEntry = _history[oldEntriesTop];
36833686
assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
3684-
if (!oldEntry.hasPage) {
3687+
if (!oldEntry.pageBased) {
36853688
// This route might need to be skipped if we can not find a page above.
36863689
pagelessRoutesToSkip += 1;
36873690
oldEntriesTop -= 1;
@@ -3715,14 +3718,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
37153718
);
37163719
// Pageless routes will be recorded when we update the middle of the old
37173720
// list.
3718-
if (!oldEntry.hasPage) {
3721+
if (!oldEntry.pageBased) {
37193722
continue;
37203723
}
37213724

3722-
assert(oldEntry.hasPage);
3723-
37243725
final Page<dynamic> page = oldEntry.route.settings as Page<dynamic>;
3725-
37263726
if (page.key == null) {
37273727
continue;
37283728
}
@@ -3749,6 +3749,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
37493749
// it into the history.
37503750
final _RouteEntry newEntry = _RouteEntry(
37513751
nextPage.createRoute(context),
3752+
pageBased: true,
37523753
initialState: _RouteLifecycle.staging,
37533754
);
37543755
needsExplicitDecision = true;
@@ -3773,7 +3774,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
37733774
final _RouteEntry potentialEntryToRemove = _history[oldEntriesBottom];
37743775
oldEntriesBottom += 1;
37753776

3776-
if (!potentialEntryToRemove.hasPage) {
3777+
if (!potentialEntryToRemove.pageBased) {
37773778
assert(previousOldPageRouteEntry != null);
37783779
final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes
37793780
.putIfAbsent(
@@ -3813,7 +3814,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
38133814
assert(() {
38143815
if (oldEntriesBottom <= oldEntriesTop) {
38153816
return newPagesBottom <= newPagesTop &&
3816-
_history[oldEntriesBottom].hasPage &&
3817+
_history[oldEntriesBottom].pageBased &&
38173818
_history[oldEntriesBottom].canUpdateFrom(widget.pages[newPagesBottom]);
38183819
} else {
38193820
return newPagesBottom > newPagesTop;
@@ -3824,7 +3825,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
38243825
while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) {
38253826
final _RouteEntry oldEntry = _history[oldEntriesBottom];
38263827
assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed);
3827-
if (!oldEntry.hasPage) {
3828+
if (!oldEntry.pageBased) {
38283829
assert(previousOldPageRouteEntry != null);
38293830
final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes
38303831
.putIfAbsent(
@@ -4459,30 +4460,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
44594460
/// state restoration.
44604461
@optionalTypeArgs
44614462
Future<T?> push<T extends Object?>(Route<T> route) {
4462-
assert(_debugCheckIsPagelessRoute(route));
4463-
_pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push));
4463+
_pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
44644464
return route.popped;
44654465
}
44664466

4467-
bool _debugCheckIsPagelessRoute(Route<dynamic> route) {
4468-
assert(() {
4469-
if (route.settings is Page) {
4470-
FlutterError.reportError(
4471-
FlutterErrorDetails(
4472-
exception: FlutterError(
4473-
'A page-based route should not be added using the imperative api. '
4474-
'Provide a new list with the corresponding Page to Navigator.pages instead.',
4475-
),
4476-
library: 'widget library',
4477-
stack: StackTrace.current,
4478-
),
4479-
);
4480-
}
4481-
return true;
4482-
}());
4483-
return true;
4484-
}
4485-
44864467
bool _debugIsStaticCallback(Function callback) {
44874468
bool result = false;
44884469
assert(() {
@@ -4610,8 +4591,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
46104591
Future<T?> pushReplacement<T extends Object?, TO extends Object?>(Route<T> newRoute, { TO? result }) {
46114592
assert(newRoute != null);
46124593
assert(newRoute._navigator == null);
4613-
assert(_debugCheckIsPagelessRoute(newRoute));
4614-
_pushReplacementEntry(_RouteEntry(newRoute, initialState: _RouteLifecycle.pushReplace), result);
4594+
_pushReplacementEntry(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.pushReplace), result);
46154595
return newRoute.popped;
46164596
}
46174597

@@ -4698,8 +4678,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
46984678
assert(newRoute != null);
46994679
assert(newRoute._navigator == null);
47004680
assert(newRoute.overlayEntries.isEmpty);
4701-
assert(_debugCheckIsPagelessRoute(newRoute));
4702-
_pushEntryAndRemoveUntil(_RouteEntry(newRoute, initialState: _RouteLifecycle.push), predicate);
4681+
_pushEntryAndRemoveUntil(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.push), predicate);
47034682
return newRoute.popped;
47044683
}
47054684

@@ -4777,7 +4756,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
47774756
assert(oldRoute != null);
47784757
assert(oldRoute._navigator == this);
47794758
assert(newRoute != null);
4780-
_replaceEntry(_RouteEntry(newRoute, initialState: _RouteLifecycle.replace), oldRoute);
4759+
_replaceEntry(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.replace), oldRoute);
47814760
}
47824761

47834762
/// Replaces a route on the navigator with a new route.
@@ -4850,7 +4829,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
48504829
assert(newRoute._navigator == null);
48514830
assert(anchorRoute != null);
48524831
assert(anchorRoute._navigator == this);
4853-
_replaceEntryBelow(_RouteEntry(newRoute, initialState: _RouteLifecycle.replace), anchorRoute);
4832+
_replaceEntryBelow(_RouteEntry(newRoute, pageBased: false, initialState: _RouteLifecycle.replace), anchorRoute);
48544833
}
48554834

48564835
/// Replaces a route on the navigator with a new route. The route to be
@@ -5004,7 +4983,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
50044983
return true;
50054984
}());
50064985
final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
5007-
if (entry.hasPage) {
4986+
if (entry.pageBased) {
50084987
if (widget.onPopPage!(entry.route, result) && entry.currentState == _RouteLifecycle.idle) {
50094988
// The entry may have been disposed if the pop finishes synchronously.
50104989
assert(entry.route._popCompleter.isCompleted);
@@ -5139,7 +5118,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin, Res
51395118
final _RouteEntry entry = _history[index];
51405119
// For page-based route with zero transition, the finalizeRoute can be
51415120
// called on any life cycle above pop.
5142-
if (entry.hasPage && entry.currentState.index < _RouteLifecycle.pop.index) {
5121+
if (entry.pageBased && entry.currentState.index < _RouteLifecycle.pop.index) {
51435122
_observedRouteDeletions.add(_NavigatorPopObservation(route, _getRouteBefore(index - 1, _RouteEntry.willBePresentPredicate)?.route));
51445123
} else {
51455124
assert(entry.currentState == _RouteLifecycle.popping);
@@ -5343,6 +5322,7 @@ abstract class _RestorationInformation {
53435322
assert(route != null);
53445323
return _RouteEntry(
53455324
route,
5325+
pageBased: false,
53465326
initialState: initialState,
53475327
restorationInformation: this,
53485328
);
@@ -5461,9 +5441,9 @@ class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
54615441
}
54625442

54635443
assert(entry.isPresentForRestoration);
5464-
if (entry.hasPage) {
5444+
if (entry.pageBased) {
54655445
needsSerialization = needsSerialization || newRoutesForCurrentPage.length != oldRoutesForCurrentPage.length;
5466-
_finalizePage(newRoutesForCurrentPage, currentPage, newMap, removedPages);
5446+
_finalizeEntry(newRoutesForCurrentPage, currentPage, newMap, removedPages);
54675447
currentPage = entry;
54685448
restorationEnabled = entry.restorationId != null;
54695449
entry.restorationEnabled = restorationEnabled;
@@ -5478,7 +5458,7 @@ class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
54785458
continue;
54795459
}
54805460

5481-
assert(!entry.hasPage);
5461+
assert(!entry.pageBased);
54825462
restorationEnabled = restorationEnabled && (entry.restorationInformation?.isRestorable ?? false);
54835463
entry.restorationEnabled = restorationEnabled;
54845464
if (restorationEnabled) {
@@ -5493,7 +5473,7 @@ class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
54935473
}
54945474
}
54955475
needsSerialization = needsSerialization || newRoutesForCurrentPage.length != oldRoutesForCurrentPage.length;
5496-
_finalizePage(newRoutesForCurrentPage, currentPage, newMap, removedPages);
5476+
_finalizeEntry(newRoutesForCurrentPage, currentPage, newMap, removedPages);
54975477

54985478
needsSerialization = needsSerialization || removedPages.isNotEmpty;
54995479

@@ -5505,13 +5485,13 @@ class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
55055485
}
55065486
}
55075487

5508-
void _finalizePage(
5488+
void _finalizeEntry(
55095489
List<Object> routes,
55105490
_RouteEntry? page,
55115491
Map<String?, List<Object>> pageToRoutes,
55125492
Set<String?> pagesToRemove,
55135493
) {
5514-
assert(page == null || page.hasPage);
5494+
assert(page == null || page.pageBased);
55155495
assert(pageToRoutes != null);
55165496
assert(!pageToRoutes.containsKey(page?.restorationId));
55175497
if (routes != null && routes.isNotEmpty) {
@@ -5549,7 +5529,7 @@ class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
55495529

55505530
List<_RouteEntry> restoreEntriesForPage(_RouteEntry? page, NavigatorState navigator) {
55515531
assert(isRegistered);
5552-
assert(page == null || page.hasPage);
5532+
assert(page == null || page.pageBased);
55535533
final List<_RouteEntry> result = <_RouteEntry>[];
55545534
if (_pageToPagelessRoutes == null || (page != null && page.restorationId == null)) {
55555535
return result;

packages/flutter/test/widgets/navigator_test.dart

+20-56
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,26 @@ void main() {
180180
expect('$exception', startsWith('Navigator operation requested with a context'));
181181
});
182182

183+
testWidgets('Navigator can push Route created through page class as Pageless route', (WidgetTester tester) async {
184+
final GlobalKey<NavigatorState> nav = GlobalKey<NavigatorState>();
185+
await tester.pumpWidget(
186+
MaterialApp(
187+
navigatorKey: nav,
188+
home: const Scaffold(
189+
body: Text('home'),
190+
)
191+
)
192+
);
193+
const MaterialPage<void> page = MaterialPage<void>(child: Text('page'));
194+
nav.currentState!.push<void>(page.createRoute(nav.currentContext!));
195+
await tester.pumpAndSettle();
196+
expect(find.text('page'), findsOneWidget);
197+
198+
nav.currentState!.pop();
199+
await tester.pumpAndSettle();
200+
expect(find.text('home'), findsOneWidget);
201+
});
202+
183203
testWidgets('Zero transition page-based route correctly notifies observers when it is popped', (WidgetTester tester) async {
184204
final List<Page<void>> pages = <Page<void>>[
185205
const ZeroTransitionPage(name: 'Page 1'),
@@ -2758,62 +2778,6 @@ void main() {
27582778
);
27592779
});
27602780

2761-
Widget buildFrame(String action) {
2762-
const TestPage myPage = TestPage(key: ValueKey<String>('1'), name:'initial');
2763-
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
2764-
'/' : (BuildContext context) => OnTapPage(
2765-
id: action,
2766-
onTap: () {
2767-
if (action == 'push') {
2768-
Navigator.of(context).push(myPage.createRoute(context));
2769-
} else if (action == 'pushReplacement') {
2770-
Navigator.of(context).pushReplacement(myPage.createRoute(context));
2771-
} else if (action == 'pushAndRemoveUntil') {
2772-
Navigator.of(context).pushAndRemoveUntil(myPage.createRoute(context), (_) => true);
2773-
}
2774-
},
2775-
),
2776-
};
2777-
2778-
return MaterialApp(routes: routes);
2779-
}
2780-
2781-
void checkException(WidgetTester tester) {
2782-
final dynamic exception = tester.takeException();
2783-
expect(exception, isFlutterError);
2784-
final FlutterError error = exception as FlutterError;
2785-
expect(
2786-
error.toStringDeep(),
2787-
equalsIgnoringHashCodes(
2788-
'FlutterError\n'
2789-
' A page-based route should not be added using the imperative api.\n'
2790-
' Provide a new list with the corresponding Page to Navigator.pages\n'
2791-
' instead.\n',
2792-
),
2793-
);
2794-
}
2795-
2796-
testWidgets('throw if add page-based route using the imperative api - push', (WidgetTester tester) async {
2797-
await tester.pumpWidget(buildFrame('push'));
2798-
await tester.tap(find.text('push'));
2799-
await tester.pumpAndSettle();
2800-
checkException(tester);
2801-
});
2802-
2803-
testWidgets('throw if add page-based route using the imperative api - pushReplacement', (WidgetTester tester) async {
2804-
await tester.pumpWidget(buildFrame('pushReplacement'));
2805-
await tester.tap(find.text('pushReplacement'));
2806-
await tester.pumpAndSettle();
2807-
checkException(tester);
2808-
});
2809-
2810-
testWidgets('throw if add page-based route using the imperative api - pushAndRemoveUntil', (WidgetTester tester) async {
2811-
await tester.pumpWidget(buildFrame('pushAndRemoveUntil'));
2812-
await tester.tap(find.text('pushAndRemoveUntil'));
2813-
await tester.pumpAndSettle();
2814-
checkException(tester);
2815-
});
2816-
28172781
testWidgets('throw if page list is empty', (WidgetTester tester) async {
28182782
final List<TestPage> myPages = <TestPage>[];
28192783
final FlutterExceptionHandler? originalOnError = FlutterError.onError;

0 commit comments

Comments
 (0)