Unverified Commit 2640bfeb authored by Mouad Debbar's avatar Mouad Debbar Committed by GitHub

Allow gaps in the initial route (#39440)

parent 0f2af976
......@@ -361,10 +361,12 @@ class WidgetsApp extends StatefulWidget {
/// also. For example, if the route was `/a/b/c`, then the app would start
/// with the three routes `/a`, `/a/b`, and `/a/b/c` loaded, in that order.
///
/// If any part of this process fails to generate routes, then the
/// [initialRoute] is ignored and [Navigator.defaultRouteName] is used instead
/// (`/`). This can happen if the app is started with an intent that specifies
/// a non-existent route.
/// Intermediate routes aren't required to exist. In the example above, `/a`
/// and `/a/b` could be skipped if they have no matching route. But `/a/b/c` is
/// required to have a route, else [initialRoute] is ignored and
/// [Navigator.defaultRouteName] is used instead (`/`). This can happen if the
/// app is started with an intent that specifies a non-existent route.
///
/// The [Navigator] is only built if routes are provided (either via [home],
/// [routes], [onGenerateRoute], or [onUnknownRoute]); if they are not,
/// [initialRoute] must be null and [builder] must not be null.
......
......@@ -783,6 +783,15 @@ class Navigator extends StatefulWidget {
/// then the [Navigator] would push the following routes on startup: `/`,
/// `/stocks`, `/stocks/HOOLI`. This enables deep linking while allowing the
/// application to maintain a predictable route history.
///
/// If any of the intermediate routes doesn't exist, it'll simply be skipped.
/// In the example above, if `/stocks` doesn't have a corresponding route in
/// the app, it'll be skipped and only `/` and `/stocks/HOOLI` will be pushed.
///
/// That said, the full route has to map to something in the app in order for
/// this to work. In our example, `/stocks/HOOLI` has to map to a route in the
/// app. Otherwise, [initialRoute] will be ignored and [defaultRouteName] will
/// be used instead.
final String initialRoute;
/// Called to generate a route for a given [RouteSettings].
......@@ -1509,9 +1518,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
if (initialRouteName.startsWith('/') && initialRouteName.length > 1) {
initialRouteName = initialRouteName.substring(1); // strip leading '/'
assert(Navigator.defaultRouteName == '/');
final List<String> plannedInitialRouteNames = <String>[
Navigator.defaultRouteName,
];
final List<Route<dynamic>> plannedInitialRoutes = <Route<dynamic>>[
_routeNamed<dynamic>(Navigator.defaultRouteName, allowNull: true, arguments: null),
];
......@@ -1520,22 +1526,17 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
String routeName = '';
for (String part in routeParts) {
routeName += '/$part';
plannedInitialRouteNames.add(routeName);
plannedInitialRoutes.add(_routeNamed<dynamic>(routeName, allowNull: true, arguments: null));
}
}
if (plannedInitialRoutes.contains(null)) {
if (plannedInitialRoutes.last == null) {
assert(() {
FlutterError.reportError(
FlutterErrorDetails(
exception:
'Could not navigate to initial route.\n'
'The requested route name was: "/$initialRouteName"\n'
'The following routes were therefore attempted:\n'
' * ${plannedInitialRouteNames.join("\n * ")}\n'
'This resulted in the following objects:\n'
' * ${plannedInitialRoutes.join("\n * ")}\n'
'One or more of those objects was null, and therefore the initial route specified will be '
'There was no corresponding route in the app, and therefore the initial route specified will be '
'ignored and "${Navigator.defaultRouteName}" will be used instead.'
),
);
......@@ -1543,7 +1544,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
}());
push(_routeNamed<Object>(Navigator.defaultRouteName, arguments: null));
} else {
plannedInitialRoutes.forEach(push);
plannedInitialRoutes.where((Route<dynamic> route) => route != null).forEach(push);
}
} else {
Route<Object> route;
......
......@@ -973,4 +973,64 @@ void main() {
expect(arguments.single, 'pushReplacementNamed');
arguments.clear();
});
testWidgets('Initial route can have gaps', (WidgetTester tester) async {
final GlobalKey<NavigatorState> keyNav = GlobalKey<NavigatorState>();
const Key keyRoot = Key('Root');
const Key keyA = Key('A');
const Key keyABC = Key('ABC');
await tester.pumpWidget(
MaterialApp(
navigatorKey: keyNav,
initialRoute: '/A/B/C',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => Container(key: keyRoot),
'/A': (BuildContext context) => Container(key: keyA),
// The route /A/B is intentionally left out.
'/A/B/C': (BuildContext context) => Container(key: keyABC),
},
),
);
// The initial route /A/B/C should've been pushed successfully.
expect(find.byKey(keyRoot), findsOneWidget);
expect(find.byKey(keyA), findsOneWidget);
expect(find.byKey(keyABC), findsOneWidget);
keyNav.currentState.pop();
await tester.pumpAndSettle();
expect(find.byKey(keyRoot), findsOneWidget);
expect(find.byKey(keyA), findsOneWidget);
expect(find.byKey(keyABC), findsNothing);
});
testWidgets('The full initial route has to be matched', (WidgetTester tester) async {
final GlobalKey<NavigatorState> keyNav = GlobalKey<NavigatorState>();
const Key keyRoot = Key('Root');
const Key keyA = Key('A');
const Key keyAB = Key('AB');
await tester.pumpWidget(
MaterialApp(
navigatorKey: keyNav,
initialRoute: '/A/B/C',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => Container(key: keyRoot),
'/A': (BuildContext context) => Container(key: keyA),
'/A/B': (BuildContext context) => Container(key: keyAB),
// The route /A/B/C is intentionally left out.
},
),
);
final dynamic exception = tester.takeException();
expect(exception is String, isTrue);
expect(exception.startsWith('Could not navigate to initial route.'), isTrue);
// Only the root route should've been pushed.
expect(find.byKey(keyRoot), findsOneWidget);
expect(find.byKey(keyA), findsNothing);
expect(find.byKey(keyAB), findsNothing);
});
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment