mirror of
https://github.com/flutter/packages.git
synced 2025-07-02 08:34:31 +08:00
[go_router] Refactored RouteMatchList and imperative APIs (#5497)
This pr refactor RouteMatchList to be a tree structure. Added a common base class RouteMatchBase. It is extended by both RouteMatch and ShellRouteMatch. The RouteMatch is for GoRoute, and is always a leaf node The ShellRouteMatch is for ShellRouteBase, and is always and intermediate node with a list of child RouteMatchBase[s]. This pr also redo how push is processed. Will add a doc explain this shortly. This is a breaking change, will write a migration guide soon. fixes https://github.com/flutter/flutter/issues/134524 fixes https://github.com/flutter/flutter/issues/130406 fixes https://github.com/flutter/flutter/issues/126365 fixes https://github.com/flutter/flutter/issues/125752 fixes https://github.com/flutter/flutter/issues/120791 fixes https://github.com/flutter/flutter/issues/120665 fixes https://github.com/flutter/flutter/issues/113001 fixes https://github.com/flutter/flutter/issues/110512
This commit is contained in:
@ -1,3 +1,10 @@
|
||||
## 13.0.0
|
||||
|
||||
- Refactors `RouteMatchList` and imperative APIs.
|
||||
- **BREAKING CHANGE**:
|
||||
- RouteMatchList structure changed.
|
||||
- Matching logic updated.
|
||||
|
||||
## 12.1.3
|
||||
|
||||
* Fixes a typo in `navigation.md`.
|
||||
|
@ -37,6 +37,7 @@ See the API documentation for details on the following topics:
|
||||
- [Error handling](https://pub.dev/documentation/go_router/latest/topics/Error%20handling-topic.html)
|
||||
|
||||
## Migration Guides
|
||||
- [Migrating to 13.0.0](https://flutter.dev/go/go-router-v13-breaking-changes).
|
||||
- [Migrating to 12.0.0](https://flutter.dev/go/go-router-v12-breaking-changes).
|
||||
- [Migrating to 11.0.0](https://flutter.dev/go/go-router-v11-breaking-changes).
|
||||
- [Migrating to 10.0.0](https://flutter.dev/go/go-router-v10-breaking-changes).
|
||||
@ -67,4 +68,3 @@ The project follows the same priority system as flutter framework.
|
||||
[P3](https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-asc+label%3Ateam-go_router+label%3AP3+)
|
||||
|
||||
[Package PRs](https://github.com/flutter/packages/pulls?q=is%3Apr+is%3Aopen+label%3A%22p%3A+go_router%22%2C%22p%3A+go_router_builder%22)
|
||||
|
||||
|
@ -68,6 +68,27 @@ Navigator.of(context).push(
|
||||
);
|
||||
```
|
||||
|
||||
The behavior may change depends on the shell route in current screen and the new screen.
|
||||
|
||||
If pushing a new screen without any shell route onto the current screen with shell route, the new
|
||||
screen is placed entirely on top of the current screen.
|
||||
|
||||

|
||||
|
||||
If pushing a new screen with the same shell route as the current screen, the new
|
||||
screen is placed inside of the shell.
|
||||
|
||||

|
||||
|
||||
If pushing a new screen with the different shell route as the current screen, the new
|
||||
screen along with the shell is placed entirely on top of the current screen.
|
||||
|
||||

|
||||
|
||||
To try out the behavior yourself, see
|
||||
[push_with_shell_route.dart](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/extra_codec.dart).
|
||||
|
||||
|
||||
## Returning values
|
||||
Waiting for a value to be returned:
|
||||
|
||||
|
160
packages/go_router/example/lib/push_with_shell_route.dart
Normal file
160
packages/go_router/example/lib/push_with_shell_route.dart
Normal file
@ -0,0 +1,160 @@
|
||||
// Copyright 2013 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:go_router/go_router.dart';
|
||||
|
||||
// This scenario demonstrates the behavior when pushing ShellRoute in various
|
||||
// scenario.
|
||||
//
|
||||
// This example have three routes, /shell1, /shell2, and /regular-route. The
|
||||
// /shell1 and /shell2 are nested in different ShellRoutes. The /regular-route
|
||||
// is a simple GoRoute.
|
||||
|
||||
void main() {
|
||||
runApp(PushWithShellRouteExampleApp());
|
||||
}
|
||||
|
||||
/// An example demonstrating how to use [ShellRoute]
|
||||
class PushWithShellRouteExampleApp extends StatelessWidget {
|
||||
/// Creates a [PushWithShellRouteExampleApp]
|
||||
PushWithShellRouteExampleApp({super.key});
|
||||
|
||||
final GoRouter _router = GoRouter(
|
||||
initialLocation: '/home',
|
||||
debugLogDiagnostics: true,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
builder: (BuildContext context, GoRouterState state, Widget child) {
|
||||
return ScaffoldForShell1(child: child);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/home',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const Home();
|
||||
},
|
||||
),
|
||||
GoRoute(
|
||||
path: '/shell1',
|
||||
pageBuilder: (_, __) => const NoTransitionPage<void>(
|
||||
child: Center(
|
||||
child: Text('shell1 body'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (BuildContext context, GoRouterState state, Widget child) {
|
||||
return ScaffoldForShell2(child: child);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/shell2',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const Center(child: Text('shell2 body'));
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/regular-route',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return const Scaffold(
|
||||
body: Center(child: Text('regular route')),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp.router(
|
||||
title: 'Flutter Demo',
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
),
|
||||
routerConfig: _router,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the "shell" for /shell1
|
||||
class ScaffoldForShell1 extends StatelessWidget {
|
||||
/// Constructs an [ScaffoldForShell1].
|
||||
const ScaffoldForShell1({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The widget to display in the body of the Scaffold.
|
||||
/// In this sample, it is a Navigator.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell1')),
|
||||
body: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the "shell" for /shell1
|
||||
class ScaffoldForShell2 extends StatelessWidget {
|
||||
/// Constructs an [ScaffoldForShell1].
|
||||
const ScaffoldForShell2({
|
||||
required this.child,
|
||||
super.key,
|
||||
});
|
||||
|
||||
/// The widget to display in the body of the Scaffold.
|
||||
/// In this sample, it is a Navigator.
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell2')),
|
||||
body: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen for /home
|
||||
class Home extends StatelessWidget {
|
||||
/// Constructs a [Home] widget.
|
||||
const Home({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push('/shell1');
|
||||
},
|
||||
child: const Text('push the same shell route /shell1'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push('/shell2');
|
||||
},
|
||||
child: const Text('push the different shell route /shell2'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
GoRouter.of(context).push('/regular-route');
|
||||
},
|
||||
child: const Text('push the regular route /regular-route'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
// Copyright 2013 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_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:go_router_examples/push_with_shell_route.dart' as example;
|
||||
|
||||
void main() {
|
||||
testWidgets('example works', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(example.PushWithShellRouteExampleApp());
|
||||
expect(find.text('shell1'), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('push the same shell route /shell1'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell1'), findsOneWidget);
|
||||
expect(find.text('shell1 body'), findsOneWidget);
|
||||
|
||||
find.text('shell1 body').evaluate().first.pop();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell1'), findsOneWidget);
|
||||
expect(find.text('shell1 body'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('push the different shell route /shell2'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell1'), findsNothing);
|
||||
expect(find.text('shell2'), findsOneWidget);
|
||||
expect(find.text('shell2 body'), findsOneWidget);
|
||||
|
||||
find.text('shell2 body').evaluate().first.pop();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell1'), findsOneWidget);
|
||||
expect(find.text('shell2'), findsNothing);
|
||||
|
||||
await tester.tap(find.text('push the regular route /regular-route'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell1'), findsNothing);
|
||||
expect(find.text('regular route'), findsOneWidget);
|
||||
|
||||
find.text('regular route').evaluate().first.pop();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell1'), findsOneWidget);
|
||||
expect(find.text('regular route'), findsNothing);
|
||||
});
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'configuration.dart';
|
||||
@ -23,6 +22,19 @@ typedef GoRouterBuilderWithNav = Widget Function(
|
||||
Widget child,
|
||||
);
|
||||
|
||||
typedef _PageBuilderForAppType = Page<void> Function({
|
||||
required LocalKey key,
|
||||
required String? name,
|
||||
required Object? arguments,
|
||||
required String restorationId,
|
||||
required Widget child,
|
||||
});
|
||||
|
||||
typedef _ErrorBuilderForAppType = Widget Function(
|
||||
BuildContext context,
|
||||
GoRouterState state,
|
||||
);
|
||||
|
||||
/// Signature for a function that takes in a `route` to be popped with
|
||||
/// the `result` and returns a boolean decision on whether the pop
|
||||
/// is successful.
|
||||
@ -32,7 +44,7 @@ typedef GoRouterBuilderWithNav = Widget Function(
|
||||
///
|
||||
/// Used by of [RouteBuilder.onPopPageWithRouteMatch].
|
||||
typedef PopPageWithRouteMatchCallback = bool Function(
|
||||
Route<dynamic> route, dynamic result, RouteMatch? match);
|
||||
Route<dynamic> route, dynamic result, RouteMatchBase match);
|
||||
|
||||
/// Builds the top-level Navigator for GoRouter.
|
||||
class RouteBuilder {
|
||||
@ -74,8 +86,6 @@ class RouteBuilder {
|
||||
/// changes.
|
||||
final List<NavigatorObserver> observers;
|
||||
|
||||
final GoRouterStateRegistry _registry = GoRouterStateRegistry();
|
||||
|
||||
/// A callback called when a `route` produced by `match` is about to be popped
|
||||
/// with the `result`.
|
||||
///
|
||||
@ -84,13 +94,6 @@ class RouteBuilder {
|
||||
/// If this method returns false, this builder aborts the pop.
|
||||
final PopPageWithRouteMatchCallback onPopPageWithRouteMatch;
|
||||
|
||||
/// Caches a HeroController for the nested Navigator, which solves cases where the
|
||||
/// Hero Widget animation stops working when navigating.
|
||||
// TODO(chunhtai): Remove _goHeroCache once below issue is fixed:
|
||||
// https://github.com/flutter/flutter/issues/54200
|
||||
final Map<GlobalKey<NavigatorState>, HeroController> _goHeroCache =
|
||||
<GlobalKey<NavigatorState>, HeroController>{};
|
||||
|
||||
/// Builds the top-level Navigator for the given [RouteMatchList].
|
||||
Widget build(
|
||||
BuildContext context,
|
||||
@ -102,339 +105,204 @@ class RouteBuilder {
|
||||
// empty box until then.
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
assert(
|
||||
matchList.isError || !(matchList.last.route as GoRoute).redirectOnly);
|
||||
assert(matchList.isError || !matchList.last.route.redirectOnly);
|
||||
return builderWithNav(
|
||||
context,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
final Map<Page<Object?>, GoRouterState> newRegistry =
|
||||
<Page<Object?>, GoRouterState>{};
|
||||
final Widget result = tryBuild(context, matchList, routerNeglect,
|
||||
configuration.navigatorKey, newRegistry);
|
||||
_registry.updateRegistry(newRegistry);
|
||||
return GoRouterStateRegistryScope(registry: _registry, child: result);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds the top-level Navigator by invoking the build method on each
|
||||
/// matching route.
|
||||
///
|
||||
/// Throws a [_RouteBuilderError].
|
||||
@visibleForTesting
|
||||
Widget tryBuild(
|
||||
BuildContext context,
|
||||
RouteMatchList matchList,
|
||||
bool routerNeglect,
|
||||
GlobalKey<NavigatorState> navigatorKey,
|
||||
Map<Page<Object?>, GoRouterState> registry,
|
||||
) {
|
||||
// TODO(chunhtai): move the state from local scope to a central place.
|
||||
// https://github.com/flutter/flutter/issues/126365
|
||||
final _PagePopContext pagePopContext =
|
||||
_PagePopContext._(onPopPageWithRouteMatch);
|
||||
return builderWithNav(
|
||||
context,
|
||||
_buildNavigator(
|
||||
pagePopContext.onPopPage,
|
||||
_buildPages(context, matchList, pagePopContext, routerNeglect,
|
||||
navigatorKey, registry),
|
||||
navigatorKey,
|
||||
_CustomNavigator(
|
||||
navigatorKey: configuration.navigatorKey,
|
||||
observers: observers,
|
||||
restorationScopeId: restorationScopeId,
|
||||
requestFocus: requestFocus,
|
||||
navigatorRestorationId: restorationScopeId,
|
||||
onPopPageWithRouteMatch: onPopPageWithRouteMatch,
|
||||
matchList: matchList,
|
||||
matches: matchList.matches,
|
||||
configuration: configuration,
|
||||
errorBuilder: errorBuilder,
|
||||
errorPageBuilder: errorPageBuilder,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the top-level pages instead of the root navigator. Used for
|
||||
/// testing.
|
||||
List<Page<Object?>> _buildPages(
|
||||
BuildContext context,
|
||||
RouteMatchList matchList,
|
||||
_PagePopContext pagePopContext,
|
||||
bool routerNeglect,
|
||||
GlobalKey<NavigatorState> navigatorKey,
|
||||
Map<Page<Object?>, GoRouterState> registry) {
|
||||
final Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPage;
|
||||
if (matchList.isError) {
|
||||
keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{
|
||||
navigatorKey: <Page<Object?>>[
|
||||
_buildErrorPage(context, _buildErrorState(matchList)),
|
||||
]
|
||||
};
|
||||
} else {
|
||||
keyToPage = <GlobalKey<NavigatorState>, List<Page<Object?>>>{};
|
||||
_buildRecursive(context, matchList, 0, pagePopContext, routerNeglect,
|
||||
keyToPage, navigatorKey, registry);
|
||||
class _CustomNavigator extends StatefulWidget {
|
||||
const _CustomNavigator({
|
||||
super.key,
|
||||
required this.navigatorKey,
|
||||
required this.observers,
|
||||
required this.navigatorRestorationId,
|
||||
required this.onPopPageWithRouteMatch,
|
||||
required this.matchList,
|
||||
required this.matches,
|
||||
required this.configuration,
|
||||
required this.errorBuilder,
|
||||
required this.errorPageBuilder,
|
||||
});
|
||||
|
||||
// Every Page should have a corresponding RouteMatch.
|
||||
assert(keyToPage.values.flattened.every((Page<Object?> page) =>
|
||||
pagePopContext.getRouteMatchesForPage(page) != null));
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
final List<NavigatorObserver> observers;
|
||||
|
||||
/// The actual [RouteMatchBase]s to be built.
|
||||
///
|
||||
/// This can be different from matches in [matchList] if this widget is used
|
||||
/// to build navigator in shell route. In this case, these matches come from
|
||||
/// the [ShellRouteMatch.matches].
|
||||
final List<RouteMatchBase> matches;
|
||||
final RouteMatchList matchList;
|
||||
final RouteConfiguration configuration;
|
||||
final PopPageWithRouteMatchCallback onPopPageWithRouteMatch;
|
||||
final String? navigatorRestorationId;
|
||||
final GoRouterWidgetBuilder? errorBuilder;
|
||||
final GoRouterPageBuilder? errorPageBuilder;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _CustomNavigatorState();
|
||||
}
|
||||
|
||||
class _CustomNavigatorState extends State<_CustomNavigator> {
|
||||
HeroController? _controller;
|
||||
late Map<Page<Object?>, RouteMatchBase> _pageToRouteMatchBase;
|
||||
final GoRouterStateRegistry _registry = GoRouterStateRegistry();
|
||||
List<Page<Object?>>? _pages;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_CustomNavigator oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.matchList != oldWidget.matchList) {
|
||||
_pages = null;
|
||||
}
|
||||
|
||||
/// Clean up previous cache to prevent memory leak, making sure any nested
|
||||
/// stateful shell routes for the current match list are kept.
|
||||
final Set<Key> activeKeys = keyToPage.keys.toSet()
|
||||
..addAll(_nestedStatefulNavigatorKeys(matchList));
|
||||
_goHeroCache.removeWhere(
|
||||
(GlobalKey<NavigatorState> key, _) => !activeKeys.contains(key));
|
||||
return keyToPage[navigatorKey]!;
|
||||
}
|
||||
|
||||
static Set<GlobalKey<NavigatorState>> _nestedStatefulNavigatorKeys(
|
||||
RouteMatchList matchList) {
|
||||
final StatefulShellRoute? shellRoute =
|
||||
matchList.routes.whereType<StatefulShellRoute>().firstOrNull;
|
||||
if (shellRoute == null) {
|
||||
return <GlobalKey<NavigatorState>>{};
|
||||
}
|
||||
return RouteBase.routesRecursively(<RouteBase>[shellRoute])
|
||||
.whereType<StatefulShellRoute>()
|
||||
.expand((StatefulShellRoute e) =>
|
||||
e.branches.map((StatefulShellBranch b) => b.navigatorKey))
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void _buildRecursive(
|
||||
BuildContext context,
|
||||
RouteMatchList matchList,
|
||||
int startIndex,
|
||||
_PagePopContext pagePopContext,
|
||||
bool routerNeglect,
|
||||
Map<GlobalKey<NavigatorState>, List<Page<Object?>>> keyToPages,
|
||||
GlobalKey<NavigatorState> navigatorKey,
|
||||
Map<Page<Object?>, GoRouterState> registry,
|
||||
) {
|
||||
if (startIndex >= matchList.matches.length) {
|
||||
return;
|
||||
}
|
||||
final RouteMatch match = matchList.matches[startIndex];
|
||||
|
||||
final RouteBase route = match.route;
|
||||
final GoRouterState state = buildState(matchList, match);
|
||||
Page<Object?>? page;
|
||||
if (state.error != null) {
|
||||
page = _buildErrorPage(context, state);
|
||||
keyToPages.putIfAbsent(navigatorKey, () => <Page<Object?>>[]).add(page);
|
||||
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
|
||||
routerNeglect, keyToPages, navigatorKey, registry);
|
||||
} else {
|
||||
// If this RouteBase is for a different Navigator, add it to the
|
||||
// list of out of scope pages
|
||||
final GlobalKey<NavigatorState> routeNavKey =
|
||||
route.parentNavigatorKey ?? navigatorKey;
|
||||
if (route is GoRoute) {
|
||||
page =
|
||||
_buildPageForGoRoute(context, state, match, route, pagePopContext);
|
||||
assert(page != null || route.redirectOnly);
|
||||
if (page != null) {
|
||||
keyToPages
|
||||
.putIfAbsent(routeNavKey, () => <Page<Object?>>[])
|
||||
.add(page);
|
||||
}
|
||||
|
||||
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
|
||||
routerNeglect, keyToPages, navigatorKey, registry);
|
||||
} else if (route is ShellRouteBase) {
|
||||
assert(startIndex + 1 < matchList.matches.length,
|
||||
'Shell routes must always have child routes');
|
||||
|
||||
// Add an entry for the parent navigator if none exists.
|
||||
//
|
||||
// Calling _buildRecursive can result in adding pages to the
|
||||
// parentNavigatorKey entry's list. Store the current length so
|
||||
// that the page for this ShellRoute is placed at the right index.
|
||||
final int shellPageIdx =
|
||||
keyToPages.putIfAbsent(routeNavKey, () => <Page<Object?>>[]).length;
|
||||
|
||||
// Find the the navigator key for the sub-route of this shell route.
|
||||
final RouteBase subRoute = matchList.matches[startIndex + 1].route;
|
||||
final GlobalKey<NavigatorState> shellNavigatorKey =
|
||||
route.navigatorKeyForSubRoute(subRoute);
|
||||
|
||||
keyToPages.putIfAbsent(shellNavigatorKey, () => <Page<Object?>>[]);
|
||||
|
||||
// Build the remaining pages
|
||||
_buildRecursive(context, matchList, startIndex + 1, pagePopContext,
|
||||
routerNeglect, keyToPages, shellNavigatorKey, registry);
|
||||
|
||||
final HeroController heroController = _goHeroCache.putIfAbsent(
|
||||
shellNavigatorKey, () => _getHeroController(context));
|
||||
|
||||
// Build the Navigator for this shell route
|
||||
Widget buildShellNavigator(
|
||||
List<NavigatorObserver>? observers,
|
||||
String? restorationScopeId, {
|
||||
bool requestFocus = true,
|
||||
}) {
|
||||
return _buildNavigator(
|
||||
pagePopContext.onPopPage,
|
||||
keyToPages[shellNavigatorKey]!,
|
||||
shellNavigatorKey,
|
||||
observers: observers ?? const <NavigatorObserver>[],
|
||||
restorationScopeId: restorationScopeId,
|
||||
heroController: heroController,
|
||||
requestFocus: requestFocus,
|
||||
);
|
||||
}
|
||||
|
||||
// Call the ShellRouteBase to create/update the shell route state
|
||||
final ShellRouteContext shellRouteContext = ShellRouteContext(
|
||||
route: route,
|
||||
routerState: state,
|
||||
navigatorKey: shellNavigatorKey,
|
||||
routeMatchList: matchList,
|
||||
navigatorBuilder: buildShellNavigator,
|
||||
);
|
||||
|
||||
// Build the Page for this route
|
||||
page = _buildPageForShellRoute(
|
||||
context, state, match, route, pagePopContext, shellRouteContext);
|
||||
// Place the ShellRoute's Page onto the list for the parent navigator.
|
||||
keyToPages[routeNavKey]!.insert(shellPageIdx, page);
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
// Create a HeroController based on the app type.
|
||||
if (_controller == null) {
|
||||
if (isMaterialApp(context)) {
|
||||
_controller = createMaterialHeroController();
|
||||
} else if (isCupertinoApp(context)) {
|
||||
_controller = createCupertinoHeroController();
|
||||
} else {
|
||||
_controller = HeroController();
|
||||
}
|
||||
}
|
||||
if (page != null) {
|
||||
registry[page] = state;
|
||||
// Insert the route match in reverse order.
|
||||
pagePopContext._insertRouteMatchAtStartForPage(page, match);
|
||||
}
|
||||
// This method can also be called if any of the page builders depend on
|
||||
// the context. In this case, make sure _pages are rebuilt.
|
||||
_pages = null;
|
||||
}
|
||||
|
||||
static Widget _buildNavigator(
|
||||
PopPageCallback onPopPage,
|
||||
List<Page<Object?>> pages,
|
||||
Key? navigatorKey, {
|
||||
List<NavigatorObserver> observers = const <NavigatorObserver>[],
|
||||
String? restorationScopeId,
|
||||
HeroController? heroController,
|
||||
bool requestFocus = true,
|
||||
}) {
|
||||
final Widget navigator = Navigator(
|
||||
key: navigatorKey,
|
||||
restorationScopeId: restorationScopeId,
|
||||
pages: pages,
|
||||
observers: observers,
|
||||
onPopPage: onPopPage,
|
||||
requestFocus: requestFocus,
|
||||
);
|
||||
if (heroController != null) {
|
||||
return HeroControllerScope(
|
||||
controller: heroController,
|
||||
child: navigator,
|
||||
);
|
||||
void _updatePages(BuildContext context) {
|
||||
assert(_pages == null);
|
||||
final List<Page<Object?>> pages = <Page<Object?>>[];
|
||||
final Map<Page<Object?>, RouteMatchBase> pageToRouteMatchBase =
|
||||
<Page<Object?>, RouteMatchBase>{};
|
||||
final Map<Page<Object?>, GoRouterState> registry =
|
||||
<Page<Object?>, GoRouterState>{};
|
||||
if (widget.matchList.isError) {
|
||||
pages.add(_buildErrorPage(context, widget.matchList));
|
||||
} else {
|
||||
return navigator;
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper method that builds a [GoRouterState] object for the given [match]
|
||||
/// and [pathParameters].
|
||||
@visibleForTesting
|
||||
GoRouterState buildState(RouteMatchList matchList, RouteMatch match) {
|
||||
final RouteBase route = match.route;
|
||||
String? name;
|
||||
String path = '';
|
||||
if (route is GoRoute) {
|
||||
name = route.name;
|
||||
path = route.path;
|
||||
}
|
||||
final RouteMatchList effectiveMatchList;
|
||||
if (match is ImperativeRouteMatch) {
|
||||
effectiveMatchList = match.matches;
|
||||
if (effectiveMatchList.isError) {
|
||||
return _buildErrorState(effectiveMatchList);
|
||||
for (final RouteMatchBase match in widget.matches) {
|
||||
final Page<Object?>? page = _buildPage(context, match);
|
||||
if (page == null) {
|
||||
continue;
|
||||
}
|
||||
pages.add(page);
|
||||
pageToRouteMatchBase[page] = match;
|
||||
registry[page] =
|
||||
match.buildState(widget.configuration, widget.matchList);
|
||||
}
|
||||
} else {
|
||||
effectiveMatchList = matchList;
|
||||
assert(!effectiveMatchList.isError);
|
||||
}
|
||||
return GoRouterState(
|
||||
configuration,
|
||||
uri: effectiveMatchList.uri,
|
||||
matchedLocation: match.matchedLocation,
|
||||
name: name,
|
||||
path: path,
|
||||
fullPath: effectiveMatchList.fullPath,
|
||||
pathParameters:
|
||||
Map<String, String>.from(effectiveMatchList.pathParameters),
|
||||
error: effectiveMatchList.error,
|
||||
extra: effectiveMatchList.extra,
|
||||
pageKey: match.pageKey,
|
||||
);
|
||||
_pages = pages;
|
||||
_registry.updateRegistry(registry);
|
||||
_pageToRouteMatchBase = pageToRouteMatchBase;
|
||||
}
|
||||
|
||||
/// Builds a [Page] for [GoRoute]
|
||||
Page<Object?>? _buildPageForGoRoute(BuildContext context, GoRouterState state,
|
||||
RouteMatch match, GoRoute route, _PagePopContext pagePopContext) {
|
||||
// Call the pageBuilder if it's non-null
|
||||
final GoRouterPageBuilder? pageBuilder = route.pageBuilder;
|
||||
Page<Object?>? _buildPage(BuildContext context, RouteMatchBase match) {
|
||||
if (match is RouteMatch) {
|
||||
if (match is ImperativeRouteMatch && match.matches.isError) {
|
||||
return _buildErrorPage(context, match.matches);
|
||||
}
|
||||
return _buildPageForGoRoute(context, match);
|
||||
}
|
||||
if (match is ShellRouteMatch) {
|
||||
return _buildPageForShellRoute(context, match);
|
||||
}
|
||||
throw GoError('unknown match type ${match.runtimeType}');
|
||||
}
|
||||
|
||||
/// Builds a [Page] for a [RouteMatch]
|
||||
Page<Object?>? _buildPageForGoRoute(BuildContext context, RouteMatch match) {
|
||||
final GoRouterPageBuilder? pageBuilder = match.route.pageBuilder;
|
||||
final GoRouterState state =
|
||||
match.buildState(widget.configuration, widget.matchList);
|
||||
if (pageBuilder != null) {
|
||||
final Page<Object?> page = pageBuilder(context, state);
|
||||
if (page is! NoOpPage) {
|
||||
return page;
|
||||
}
|
||||
}
|
||||
return _callGoRouteBuilder(context, state, route);
|
||||
}
|
||||
|
||||
/// Calls the user-provided route builder from the [GoRoute].
|
||||
Page<Object?>? _callGoRouteBuilder(
|
||||
BuildContext context, GoRouterState state, GoRoute route) {
|
||||
final GoRouterWidgetBuilder? builder = route.builder;
|
||||
final GoRouterWidgetBuilder? builder = match.route.builder;
|
||||
|
||||
if (builder == null) {
|
||||
return null;
|
||||
}
|
||||
return buildPage(context, state, Builder(builder: (BuildContext context) {
|
||||
return _buildPlatformAdapterPage(context, state,
|
||||
Builder(builder: (BuildContext context) {
|
||||
return builder(context, state);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Builds a [Page] for [ShellRouteBase]
|
||||
/// Builds a [Page] for a [ShellRouteMatch]
|
||||
Page<Object?> _buildPageForShellRoute(
|
||||
BuildContext context,
|
||||
GoRouterState state,
|
||||
RouteMatch match,
|
||||
ShellRouteBase route,
|
||||
_PagePopContext pagePopContext,
|
||||
ShellRouteContext shellRouteContext) {
|
||||
Page<Object?>? page = route.buildPage(context, state, shellRouteContext);
|
||||
if (page is NoOpPage) {
|
||||
page = null;
|
||||
BuildContext context,
|
||||
ShellRouteMatch match,
|
||||
) {
|
||||
final GoRouterState state =
|
||||
match.buildState(widget.configuration, widget.matchList);
|
||||
final GlobalKey<NavigatorState> navigatorKey = match.navigatorKey;
|
||||
final ShellRouteContext shellRouteContext = ShellRouteContext(
|
||||
route: match.route,
|
||||
routerState: state,
|
||||
navigatorKey: navigatorKey,
|
||||
routeMatchList: widget.matchList,
|
||||
navigatorBuilder:
|
||||
(List<NavigatorObserver>? observers, String? restorationScopeId) {
|
||||
return _CustomNavigator(
|
||||
// The state needs to persist across rebuild.
|
||||
key: GlobalObjectKey(navigatorKey.hashCode),
|
||||
navigatorRestorationId: restorationScopeId,
|
||||
navigatorKey: navigatorKey,
|
||||
matches: match.matches,
|
||||
matchList: widget.matchList,
|
||||
configuration: widget.configuration,
|
||||
observers: observers ?? const <NavigatorObserver>[],
|
||||
onPopPageWithRouteMatch: widget.onPopPageWithRouteMatch,
|
||||
// This is used to recursively build pages under this shell route.
|
||||
errorBuilder: widget.errorBuilder,
|
||||
errorPageBuilder: widget.errorPageBuilder,
|
||||
);
|
||||
},
|
||||
);
|
||||
final Page<Object?>? page =
|
||||
match.route.buildPage(context, state, shellRouteContext);
|
||||
if (page != null && page is! NoOpPage) {
|
||||
return page;
|
||||
}
|
||||
|
||||
// Return the result of the route's builder() or pageBuilder()
|
||||
return page ??
|
||||
buildPage(context, state, Builder(builder: (BuildContext context) {
|
||||
return _callShellRouteBaseBuilder(
|
||||
context, state, route, shellRouteContext);
|
||||
}));
|
||||
}
|
||||
|
||||
/// Calls the user-provided route builder from the [ShellRouteBase].
|
||||
Widget _callShellRouteBaseBuilder(BuildContext context, GoRouterState state,
|
||||
ShellRouteBase route, ShellRouteContext? shellRouteContext) {
|
||||
assert(shellRouteContext != null,
|
||||
'ShellRouteContext must be provided for ${route.runtimeType}');
|
||||
final Widget? widget =
|
||||
route.buildWidget(context, state, shellRouteContext!);
|
||||
if (widget == null) {
|
||||
throw GoError('No builder provided to ShellRoute: $route');
|
||||
}
|
||||
|
||||
return widget;
|
||||
return _buildPlatformAdapterPage(
|
||||
context,
|
||||
state,
|
||||
Builder(
|
||||
builder: (BuildContext context) {
|
||||
return match.route.buildWidget(context, state, shellRouteContext)!;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_PageBuilderForAppType? _pageBuilderForAppType;
|
||||
|
||||
Widget Function(
|
||||
BuildContext context,
|
||||
GoRouterState state,
|
||||
)? _errorBuilderForAppType;
|
||||
_ErrorBuilderForAppType? _errorBuilderForAppType;
|
||||
|
||||
void _cacheAppType(BuildContext context) {
|
||||
// cache app type-specific page and error builders
|
||||
@ -456,7 +324,20 @@ class RouteBuilder {
|
||||
(BuildContext c, GoRouterState s) => CupertinoErrorScreen(s.error);
|
||||
} else {
|
||||
log('Using WidgetsApp configuration');
|
||||
_pageBuilderForAppType = pageBuilderForWidgetApp;
|
||||
_pageBuilderForAppType = ({
|
||||
required LocalKey key,
|
||||
required String? name,
|
||||
required Object? arguments,
|
||||
required String restorationId,
|
||||
required Widget child,
|
||||
}) =>
|
||||
NoTransitionPage<void>(
|
||||
name: name,
|
||||
arguments: arguments,
|
||||
key: key,
|
||||
restorationId: restorationId,
|
||||
child: child,
|
||||
);
|
||||
_errorBuilderForAppType =
|
||||
(BuildContext c, GoRouterState s) => ErrorScreen(s.error);
|
||||
}
|
||||
@ -467,8 +348,7 @@ class RouteBuilder {
|
||||
}
|
||||
|
||||
/// builds the page based on app type, i.e. MaterialApp vs. CupertinoApp
|
||||
@visibleForTesting
|
||||
Page<Object?> buildPage(
|
||||
Page<Object?> _buildPlatformAdapterPage(
|
||||
BuildContext context,
|
||||
GoRouterState state,
|
||||
Widget child,
|
||||
@ -487,26 +367,10 @@ class RouteBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
/// Builds a page without any transitions.
|
||||
Page<void> pageBuilderForWidgetApp({
|
||||
required LocalKey key,
|
||||
required String? name,
|
||||
required Object? arguments,
|
||||
required String restorationId,
|
||||
required Widget child,
|
||||
}) =>
|
||||
NoTransitionPage<void>(
|
||||
name: name,
|
||||
arguments: arguments,
|
||||
key: key,
|
||||
restorationId: restorationId,
|
||||
child: child,
|
||||
);
|
||||
|
||||
GoRouterState _buildErrorState(RouteMatchList matchList) {
|
||||
assert(matchList.isError);
|
||||
return GoRouterState(
|
||||
configuration,
|
||||
widget.configuration,
|
||||
uri: matchList.uri,
|
||||
matchedLocation: matchList.uri.path,
|
||||
fullPath: matchList.fullPath,
|
||||
@ -517,7 +381,8 @@ class RouteBuilder {
|
||||
}
|
||||
|
||||
/// Builds a an error page.
|
||||
Page<void> _buildErrorPage(BuildContext context, GoRouterState state) {
|
||||
Page<void> _buildErrorPage(BuildContext context, RouteMatchList matchList) {
|
||||
final GoRouterState state = _buildErrorState(matchList);
|
||||
assert(state.error != null);
|
||||
|
||||
// If the error page builder is provided, use that, otherwise, if the error
|
||||
@ -525,10 +390,10 @@ class RouteBuilder {
|
||||
// MaterialPage). Finally, if nothing is provided, use a default error page
|
||||
// wrapped in the app-specific page.
|
||||
_cacheAppType(context);
|
||||
final GoRouterWidgetBuilder? errorBuilder = this.errorBuilder;
|
||||
return errorPageBuilder != null
|
||||
? errorPageBuilder!(context, state)
|
||||
: buildPage(
|
||||
final GoRouterWidgetBuilder? errorBuilder = widget.errorBuilder;
|
||||
return widget.errorPageBuilder != null
|
||||
? widget.errorPageBuilder!(context, state)
|
||||
: _buildPlatformAdapterPage(
|
||||
context,
|
||||
state,
|
||||
errorBuilder != null
|
||||
@ -537,66 +402,30 @@ class RouteBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
/// Return a HeroController based on the app type.
|
||||
HeroController _getHeroController(BuildContext context) {
|
||||
if (context is Element) {
|
||||
if (isMaterialApp(context)) {
|
||||
return createMaterialHeroController();
|
||||
} else if (isCupertinoApp(context)) {
|
||||
return createCupertinoHeroController();
|
||||
}
|
||||
}
|
||||
return HeroController();
|
||||
}
|
||||
}
|
||||
|
||||
typedef _PageBuilderForAppType = Page<void> Function({
|
||||
required LocalKey key,
|
||||
required String? name,
|
||||
required Object? arguments,
|
||||
required String restorationId,
|
||||
required Widget child,
|
||||
});
|
||||
|
||||
/// Context used to provide a route to page association when popping routes.
|
||||
class _PagePopContext {
|
||||
_PagePopContext._(this.onPopPageWithRouteMatch);
|
||||
|
||||
/// A page can be mapped to a RouteMatch list, such as a const page being
|
||||
/// pushed multiple times.
|
||||
final Map<Page<dynamic>, List<RouteMatch>> _routeMatchesLookUp =
|
||||
<Page<Object?>, List<RouteMatch>>{};
|
||||
|
||||
/// On pop page callback that includes the associated [RouteMatch].
|
||||
final PopPageWithRouteMatchCallback onPopPageWithRouteMatch;
|
||||
|
||||
/// Looks for the [RouteMatch] for a given [Page].
|
||||
///
|
||||
/// The [Page] must have been previously built via the [RouteBuilder] that
|
||||
/// created this [PagePopContext]; otherwise, this method returns null.
|
||||
List<RouteMatch>? getRouteMatchesForPage(Page<Object?> page) =>
|
||||
_routeMatchesLookUp[page];
|
||||
|
||||
/// This is called in _buildRecursive to insert route matches in reverse order.
|
||||
void _insertRouteMatchAtStartForPage(Page<Object?> page, RouteMatch match) {
|
||||
_routeMatchesLookUp
|
||||
.putIfAbsent(page, () => <RouteMatch>[])
|
||||
.insert(0, match);
|
||||
}
|
||||
|
||||
/// Function used as [Navigator.onPopPage] callback when creating Navigators.
|
||||
///
|
||||
/// This function forwards to [onPopPageWithRouteMatch], including the
|
||||
/// [RouteMatch] associated with the popped route.
|
||||
///
|
||||
/// This assumes always pop the last route match for the page.
|
||||
bool onPopPage(Route<dynamic> route, dynamic result) {
|
||||
bool _handlePopPage(Route<Object?> route, Object? result) {
|
||||
final Page<Object?> page = route.settings as Page<Object?>;
|
||||
final RouteMatch match = _routeMatchesLookUp[page]!.last;
|
||||
if (onPopPageWithRouteMatch(route, result, match)) {
|
||||
_routeMatchesLookUp[page]!.removeLast();
|
||||
return true;
|
||||
final RouteMatchBase match = _pageToRouteMatchBase[page]!;
|
||||
return widget.onPopPageWithRouteMatch(route, result, match);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_pages == null) {
|
||||
_updatePages(context);
|
||||
}
|
||||
return false;
|
||||
assert(_pages != null);
|
||||
return GoRouterStateRegistryScope(
|
||||
registry: _registry,
|
||||
child: HeroControllerScope(
|
||||
controller: _controller!,
|
||||
child: Navigator(
|
||||
key: widget.navigatorKey,
|
||||
restorationScopeId: widget.navigatorRestorationId,
|
||||
pages: _pages!,
|
||||
observers: widget.observers,
|
||||
onPopPage: _handlePopPage,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -295,9 +295,10 @@ class RouteConfiguration {
|
||||
final Uri uri = Uri.parse(canonicalUri(location));
|
||||
|
||||
final Map<String, String> pathParameters = <String, String>{};
|
||||
final List<RouteMatch>? matches = _getLocRouteMatches(uri, pathParameters);
|
||||
final List<RouteMatchBase> matches =
|
||||
_getLocRouteMatches(uri, pathParameters);
|
||||
|
||||
if (matches == null) {
|
||||
if (matches.isEmpty) {
|
||||
return _errorRouteMatchList(
|
||||
uri,
|
||||
GoException('no routes for location: $uri'),
|
||||
@ -328,96 +329,20 @@ class RouteConfiguration {
|
||||
return result;
|
||||
}
|
||||
|
||||
List<RouteMatch>? _getLocRouteMatches(
|
||||
List<RouteMatchBase> _getLocRouteMatches(
|
||||
Uri uri, Map<String, String> pathParameters) {
|
||||
final List<RouteMatch>? result = _getLocRouteRecursively(
|
||||
location: uri.path,
|
||||
remainingLocation: uri.path,
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
pathParameters: pathParameters,
|
||||
routes: _routingConfig.value.routes,
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
List<RouteMatch>? _getLocRouteRecursively({
|
||||
required String location,
|
||||
required String remainingLocation,
|
||||
required String matchedLocation,
|
||||
required String matchedPath,
|
||||
required Map<String, String> pathParameters,
|
||||
required List<RouteBase> routes,
|
||||
}) {
|
||||
List<RouteMatch>? result;
|
||||
late Map<String, String> subPathParameters;
|
||||
// find the set of matches at this level of the tree
|
||||
for (final RouteBase route in routes) {
|
||||
subPathParameters = <String, String>{};
|
||||
|
||||
final RouteMatch? match = RouteMatch.match(
|
||||
for (final RouteBase route in _routingConfig.value.routes) {
|
||||
final List<RouteMatchBase> result = RouteMatchBase.match(
|
||||
rootNavigatorKey: navigatorKey,
|
||||
route: route,
|
||||
remainingLocation: remainingLocation,
|
||||
matchedLocation: matchedLocation,
|
||||
matchedPath: matchedPath,
|
||||
pathParameters: subPathParameters,
|
||||
uri: uri,
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
|
||||
if (match == null) {
|
||||
continue;
|
||||
if (result.isNotEmpty) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (match.route is GoRoute &&
|
||||
match.matchedLocation.toLowerCase() == location.toLowerCase()) {
|
||||
// If it is a complete match, then return the matched route
|
||||
// NOTE: need a lower case match because matchedLocation is canonicalized to match
|
||||
// the path case whereas the location can be of any case and still match
|
||||
result = <RouteMatch>[match];
|
||||
} else if (route.routes.isEmpty) {
|
||||
// If it is partial match but no sub-routes, bail.
|
||||
continue;
|
||||
} else {
|
||||
// Otherwise, recurse
|
||||
final String childRestLoc;
|
||||
final String newParentSubLoc;
|
||||
final String newParentPath;
|
||||
if (match.route is ShellRouteBase) {
|
||||
childRestLoc = remainingLocation;
|
||||
newParentSubLoc = matchedLocation;
|
||||
newParentPath = matchedPath;
|
||||
} else {
|
||||
assert(location.startsWith(match.matchedLocation));
|
||||
assert(remainingLocation.isNotEmpty);
|
||||
|
||||
childRestLoc = location.substring(match.matchedLocation.length +
|
||||
(match.matchedLocation == '/' ? 0 : 1));
|
||||
newParentSubLoc = match.matchedLocation;
|
||||
newParentPath =
|
||||
concatenatePaths(matchedPath, (match.route as GoRoute).path);
|
||||
}
|
||||
|
||||
final List<RouteMatch>? subRouteMatch = _getLocRouteRecursively(
|
||||
location: location,
|
||||
remainingLocation: childRestLoc,
|
||||
matchedLocation: newParentSubLoc,
|
||||
matchedPath: newParentPath,
|
||||
pathParameters: subPathParameters,
|
||||
routes: route.routes,
|
||||
);
|
||||
|
||||
// If there's no sub-route matches, there is no match for this location
|
||||
if (subRouteMatch == null) {
|
||||
continue;
|
||||
}
|
||||
result = <RouteMatch>[match, ...subRouteMatch];
|
||||
}
|
||||
// Should only reach here if there is a match.
|
||||
break;
|
||||
}
|
||||
if (result != null) {
|
||||
pathParameters.addAll(subPathParameters);
|
||||
}
|
||||
return result;
|
||||
return const <RouteMatchBase>[];
|
||||
}
|
||||
|
||||
/// Processes redirects by returning a new [RouteMatchList] representing the new
|
||||
@ -468,8 +393,17 @@ class RouteConfiguration {
|
||||
return prevMatchList;
|
||||
}
|
||||
|
||||
final List<RouteMatch> routeMatches = <RouteMatch>[];
|
||||
prevMatchList.visitRouteMatches((RouteMatchBase match) {
|
||||
if (match is RouteMatch) {
|
||||
routeMatches.add(match);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
final FutureOr<String?> routeLevelRedirectResult =
|
||||
_getRouteLevelRedirect(context, prevMatchList, 0);
|
||||
_getRouteLevelRedirect(context, prevMatchList, routeMatches, 0);
|
||||
|
||||
if (routeLevelRedirectResult is String?) {
|
||||
return processRouteLevelRedirect(routeLevelRedirectResult);
|
||||
}
|
||||
@ -499,33 +433,23 @@ class RouteConfiguration {
|
||||
FutureOr<String?> _getRouteLevelRedirect(
|
||||
BuildContext context,
|
||||
RouteMatchList matchList,
|
||||
List<RouteMatch> routeMatches,
|
||||
int currentCheckIndex,
|
||||
) {
|
||||
if (currentCheckIndex >= matchList.matches.length) {
|
||||
if (currentCheckIndex >= routeMatches.length) {
|
||||
return null;
|
||||
}
|
||||
final RouteMatch match = matchList.matches[currentCheckIndex];
|
||||
final RouteMatch match = routeMatches[currentCheckIndex];
|
||||
FutureOr<String?> processRouteRedirect(String? newLocation) =>
|
||||
newLocation ??
|
||||
_getRouteLevelRedirect(context, matchList, currentCheckIndex + 1);
|
||||
final RouteBase route = match.route;
|
||||
_getRouteLevelRedirect(
|
||||
context, matchList, routeMatches, currentCheckIndex + 1);
|
||||
final GoRoute route = match.route;
|
||||
FutureOr<String?> routeRedirectResult;
|
||||
if (route is GoRoute && route.redirect != null) {
|
||||
final RouteMatchList effectiveMatchList =
|
||||
match is ImperativeRouteMatch ? match.matches : matchList;
|
||||
if (route.redirect != null) {
|
||||
routeRedirectResult = route.redirect!(
|
||||
context,
|
||||
GoRouterState(
|
||||
this,
|
||||
uri: effectiveMatchList.uri,
|
||||
matchedLocation: match.matchedLocation,
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
fullPath: effectiveMatchList.fullPath,
|
||||
extra: effectiveMatchList.extra,
|
||||
pathParameters: effectiveMatchList.pathParameters,
|
||||
pageKey: match.pageKey,
|
||||
),
|
||||
match.buildState(this, matchList),
|
||||
);
|
||||
}
|
||||
if (routeRedirectResult is String?) {
|
||||
|
@ -50,21 +50,27 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
||||
|
||||
final RouteConfiguration _configuration;
|
||||
|
||||
_NavigatorStateIterator _createNavigatorStateIterator() =>
|
||||
_NavigatorStateIterator(currentConfiguration, navigatorKey.currentState!);
|
||||
|
||||
@override
|
||||
Future<bool> popRoute() async {
|
||||
final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
|
||||
while (iterator.moveNext()) {
|
||||
final bool didPop = await iterator.current.maybePop();
|
||||
if (didPop) {
|
||||
return true;
|
||||
NavigatorState? state = navigatorKey.currentState;
|
||||
if (state == null) {
|
||||
return false;
|
||||
}
|
||||
if (!state.canPop()) {
|
||||
state = null;
|
||||
}
|
||||
RouteMatchBase walker = currentConfiguration.matches.last;
|
||||
while (walker is ShellRouteMatch) {
|
||||
if (walker.navigatorKey.currentState?.canPop() ?? false) {
|
||||
state = walker.navigatorKey.currentState;
|
||||
}
|
||||
walker = walker.matches.last;
|
||||
}
|
||||
if (state != null) {
|
||||
return state.maybePop();
|
||||
}
|
||||
// This should be the only place where the last GoRoute exit the screen.
|
||||
final GoRoute lastRoute =
|
||||
currentConfiguration.matches.last.route as GoRoute;
|
||||
final GoRoute lastRoute = currentConfiguration.last.route;
|
||||
if (lastRoute.onExit != null && navigatorKey.currentContext != null) {
|
||||
return !(await lastRoute.onExit!(navigatorKey.currentContext!));
|
||||
}
|
||||
@ -73,25 +79,36 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
||||
|
||||
/// Returns `true` if the active Navigator can pop.
|
||||
bool canPop() {
|
||||
final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
|
||||
while (iterator.moveNext()) {
|
||||
if (iterator.current.canPop()) {
|
||||
if (navigatorKey.currentState?.canPop() ?? false) {
|
||||
return true;
|
||||
}
|
||||
RouteMatchBase walker = currentConfiguration.matches.last;
|
||||
while (walker is ShellRouteMatch) {
|
||||
if (walker.navigatorKey.currentState?.canPop() ?? false) {
|
||||
return true;
|
||||
}
|
||||
walker = walker.matches.last;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// Pops the top-most route.
|
||||
void pop<T extends Object?>([T? result]) {
|
||||
final _NavigatorStateIterator iterator = _createNavigatorStateIterator();
|
||||
while (iterator.moveNext()) {
|
||||
if (iterator.current.canPop()) {
|
||||
iterator.current.pop<T>(result);
|
||||
return;
|
||||
}
|
||||
NavigatorState? state;
|
||||
if (navigatorKey.currentState?.canPop() ?? false) {
|
||||
state = navigatorKey.currentState;
|
||||
}
|
||||
throw GoError('There is nothing to pop');
|
||||
RouteMatchBase walker = currentConfiguration.matches.last;
|
||||
while (walker is ShellRouteMatch) {
|
||||
if (walker.navigatorKey.currentState?.canPop() ?? false) {
|
||||
state = walker.navigatorKey.currentState;
|
||||
}
|
||||
walker = walker.matches.last;
|
||||
}
|
||||
if (state == null) {
|
||||
throw GoError('There is nothing to pop');
|
||||
}
|
||||
state.pop(result);
|
||||
}
|
||||
|
||||
void _debugAssertMatchListNotEmpty() {
|
||||
@ -103,14 +120,13 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
||||
}
|
||||
|
||||
bool _handlePopPageWithRouteMatch(
|
||||
Route<Object?> route, Object? result, RouteMatch? match) {
|
||||
Route<Object?> route, Object? result, RouteMatchBase match) {
|
||||
if (route.willHandlePopInternally) {
|
||||
final bool popped = route.didPop(result);
|
||||
assert(!popped);
|
||||
return popped;
|
||||
}
|
||||
assert(match != null);
|
||||
final RouteBase routeBase = match!.route;
|
||||
final RouteBase routeBase = match.route;
|
||||
if (routeBase is! GoRoute || routeBase.onExit == null) {
|
||||
route.didPop(result);
|
||||
_completeRouteMatch(result, match);
|
||||
@ -130,7 +146,7 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
||||
return false;
|
||||
}
|
||||
|
||||
void _completeRouteMatch(Object? result, RouteMatch match) {
|
||||
void _completeRouteMatch(Object? result, RouteMatchBase match) {
|
||||
if (match is ImperativeRouteMatch) {
|
||||
match.complete(result);
|
||||
}
|
||||
@ -173,25 +189,41 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
||||
final BuildContext? navigatorContext = navigatorKey.currentContext;
|
||||
// If navigator is not built or disposed, the GoRoute.onExit is irrelevant.
|
||||
if (navigatorContext != null) {
|
||||
final List<RouteMatch> currentGoRouteMatches = <RouteMatch>[];
|
||||
currentConfiguration.visitRouteMatches((RouteMatchBase match) {
|
||||
if (match is RouteMatch) {
|
||||
currentGoRouteMatches.add(match);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
final List<RouteMatch> newGoRouteMatches = <RouteMatch>[];
|
||||
configuration.visitRouteMatches((RouteMatchBase match) {
|
||||
if (match is RouteMatch) {
|
||||
newGoRouteMatches.add(match);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
final int compareUntil = math.min(
|
||||
currentConfiguration.matches.length,
|
||||
configuration.matches.length,
|
||||
currentGoRouteMatches.length,
|
||||
newGoRouteMatches.length,
|
||||
);
|
||||
int indexOfFirstDiff = 0;
|
||||
for (; indexOfFirstDiff < compareUntil; indexOfFirstDiff++) {
|
||||
if (currentConfiguration.matches[indexOfFirstDiff] !=
|
||||
configuration.matches[indexOfFirstDiff]) {
|
||||
if (currentGoRouteMatches[indexOfFirstDiff] !=
|
||||
newGoRouteMatches[indexOfFirstDiff]) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (indexOfFirstDiff < currentConfiguration.matches.length) {
|
||||
final List<GoRoute> exitingGoRoutes = currentConfiguration.matches
|
||||
|
||||
if (indexOfFirstDiff < currentGoRouteMatches.length) {
|
||||
final List<GoRoute> exitingGoRoutes = currentGoRouteMatches
|
||||
.sublist(indexOfFirstDiff)
|
||||
.map<RouteBase>((RouteMatch match) => match.route)
|
||||
.whereType<GoRoute>()
|
||||
.toList();
|
||||
return _callOnExitStartsAt(exitingGoRoutes.length - 1,
|
||||
navigatorContext: navigatorContext, routes: exitingGoRoutes)
|
||||
context: navigatorContext, routes: exitingGoRoutes)
|
||||
.then<void>((bool exit) {
|
||||
if (!exit) {
|
||||
return SynchronousFuture<void>(null);
|
||||
@ -209,25 +241,23 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
||||
/// The returned future resolves to true if all routes below the index all
|
||||
/// return true. Otherwise, the returned future resolves to false.
|
||||
static Future<bool> _callOnExitStartsAt(int index,
|
||||
{required BuildContext navigatorContext, required List<GoRoute> routes}) {
|
||||
{required BuildContext context, required List<GoRoute> routes}) {
|
||||
if (index < 0) {
|
||||
return SynchronousFuture<bool>(true);
|
||||
}
|
||||
final GoRoute goRoute = routes[index];
|
||||
if (goRoute.onExit == null) {
|
||||
return _callOnExitStartsAt(index - 1,
|
||||
navigatorContext: navigatorContext, routes: routes);
|
||||
return _callOnExitStartsAt(index - 1, context: context, routes: routes);
|
||||
}
|
||||
|
||||
Future<bool> handleOnExitResult(bool exit) {
|
||||
if (exit) {
|
||||
return _callOnExitStartsAt(index - 1,
|
||||
navigatorContext: navigatorContext, routes: routes);
|
||||
return _callOnExitStartsAt(index - 1, context: context, routes: routes);
|
||||
}
|
||||
return SynchronousFuture<bool>(false);
|
||||
}
|
||||
|
||||
final FutureOr<bool> exitFuture = goRoute.onExit!(navigatorContext);
|
||||
final FutureOr<bool> exitFuture = goRoute.onExit!(context);
|
||||
if (exitFuture is bool) {
|
||||
return handleOnExitResult(exitFuture);
|
||||
}
|
||||
@ -240,73 +270,3 @@ class GoRouterDelegate extends RouterDelegate<RouteMatchList>
|
||||
return SynchronousFuture<void>(null);
|
||||
}
|
||||
}
|
||||
|
||||
/// An iterator that iterates through navigators that [GoRouterDelegate]
|
||||
/// created from the inner to outer.
|
||||
///
|
||||
/// The iterator starts with the navigator that hosts the top-most route. This
|
||||
/// navigator may not be the inner-most navigator if the top-most route is a
|
||||
/// pageless route, such as a dialog or bottom sheet.
|
||||
class _NavigatorStateIterator implements Iterator<NavigatorState> {
|
||||
_NavigatorStateIterator(this.matchList, this.root)
|
||||
: index = matchList.matches.length - 1;
|
||||
|
||||
final RouteMatchList matchList;
|
||||
int index;
|
||||
|
||||
final NavigatorState root;
|
||||
@override
|
||||
late NavigatorState current;
|
||||
|
||||
RouteBase _getRouteAtIndex(int index) => matchList.matches[index].route;
|
||||
|
||||
void _findsNextIndex() {
|
||||
final GlobalKey<NavigatorState>? parentNavigatorKey =
|
||||
_getRouteAtIndex(index).parentNavigatorKey;
|
||||
if (parentNavigatorKey == null) {
|
||||
index -= 1;
|
||||
return;
|
||||
}
|
||||
|
||||
for (index -= 1; index >= 0; index -= 1) {
|
||||
final RouteBase route = _getRouteAtIndex(index);
|
||||
if (route is ShellRouteBase) {
|
||||
if (route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1)) ==
|
||||
parentNavigatorKey) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(root == parentNavigatorKey.currentState);
|
||||
}
|
||||
|
||||
@override
|
||||
bool moveNext() {
|
||||
if (index < 0) {
|
||||
return false;
|
||||
}
|
||||
_findsNextIndex();
|
||||
|
||||
while (index >= 0) {
|
||||
final RouteBase route = _getRouteAtIndex(index);
|
||||
if (route is ShellRouteBase) {
|
||||
final GlobalKey<NavigatorState> navigatorKey =
|
||||
route.navigatorKeyForSubRoute(_getRouteAtIndex(index + 1));
|
||||
// Must have a ModalRoute parent because the navigator ShellRoute
|
||||
// created must not be the root navigator.
|
||||
final ModalRoute<Object?> parentModalRoute =
|
||||
ModalRoute.of(navigatorKey.currentContext!)!;
|
||||
// There may be pageless route on top of ModalRoute that the
|
||||
// parentNavigatorKey is in. For example an open dialog.
|
||||
if (parentModalRoute.isCurrent) {
|
||||
current = navigatorKey.currentState!;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
_findsNextIndex();
|
||||
}
|
||||
assert(index == -1);
|
||||
current = root;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -16,65 +16,23 @@ import 'logging.dart';
|
||||
import 'misc/errors.dart';
|
||||
import 'path_utils.dart';
|
||||
import 'route.dart';
|
||||
import 'state.dart';
|
||||
|
||||
/// An matched result by matching a [RouteBase] against a location.
|
||||
/// The function signature for [RouteMatchList.visitRouteMatches]
|
||||
///
|
||||
/// This is typically created by calling [RouteMatch.match].
|
||||
@immutable
|
||||
class RouteMatch {
|
||||
/// Constructor for [RouteMatch].
|
||||
const RouteMatch({
|
||||
required this.route,
|
||||
required this.matchedLocation,
|
||||
required this.pageKey,
|
||||
});
|
||||
/// Return false to stop the walk.
|
||||
typedef RouteMatchVisitor = bool Function(RouteMatchBase);
|
||||
|
||||
/// Generate a [RouteMatch] object by matching the `route` with
|
||||
/// `remainingLocation`.
|
||||
///
|
||||
/// The extracted path parameters, as the result of the matching, are stored
|
||||
/// into `pathParameters`.
|
||||
static RouteMatch? match({
|
||||
required RouteBase route,
|
||||
required String matchedPath, // e.g. /family/:fid
|
||||
required String remainingLocation, // e.g. person/p1
|
||||
required String matchedLocation, // e.g. /family/f2
|
||||
required Map<String, String> pathParameters,
|
||||
}) {
|
||||
if (route is ShellRouteBase) {
|
||||
return RouteMatch(
|
||||
route: route,
|
||||
matchedLocation: remainingLocation,
|
||||
pageKey: ValueKey<String>(route.hashCode.toString()),
|
||||
);
|
||||
} else if (route is GoRoute) {
|
||||
assert(!route.path.contains('//'));
|
||||
|
||||
final RegExpMatch? match = route.matchPatternAsPrefix(remainingLocation);
|
||||
if (match == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Map<String, String> encodedParams = route.extractPathParams(match);
|
||||
for (final MapEntry<String, String> param in encodedParams.entries) {
|
||||
pathParameters[param.key] = Uri.decodeComponent(param.value);
|
||||
}
|
||||
final String pathLoc = patternToPath(route.path, encodedParams);
|
||||
final String newMatchedLocation =
|
||||
concatenatePaths(matchedLocation, pathLoc);
|
||||
final String newMatchedPath = concatenatePaths(matchedPath, route.path);
|
||||
return RouteMatch(
|
||||
route: route,
|
||||
matchedLocation: newMatchedLocation,
|
||||
pageKey: ValueKey<String>(newMatchedPath),
|
||||
);
|
||||
}
|
||||
assert(false, 'Unexpected route type: $route');
|
||||
return null;
|
||||
}
|
||||
/// The base class for various route matches.
|
||||
abstract class RouteMatchBase with Diagnosticable {
|
||||
/// An abstract route match base
|
||||
const RouteMatchBase();
|
||||
|
||||
/// The matched route.
|
||||
final RouteBase route;
|
||||
RouteBase get route;
|
||||
|
||||
/// The page key.
|
||||
ValueKey<String> get pageKey;
|
||||
|
||||
/// The location string that matches the [route].
|
||||
///
|
||||
@ -84,9 +42,260 @@ class RouteMatch {
|
||||
/// route = GoRoute('/family/:id')
|
||||
///
|
||||
/// matchedLocation = '/family/f2'
|
||||
String get matchedLocation;
|
||||
|
||||
/// Gets the state that represent this route match.
|
||||
GoRouterState buildState(
|
||||
RouteConfiguration configuration, RouteMatchList matches);
|
||||
|
||||
/// Generates a list of [RouteMatchBase] objects by matching the `route` and
|
||||
/// its sub-routes with `uri`.
|
||||
///
|
||||
/// This method returns empty list if it can't find a complete match in the
|
||||
/// `route`.
|
||||
///
|
||||
/// The `rootNavigatorKey` is required to match routes with
|
||||
/// parentNavigatorKey.
|
||||
///
|
||||
/// The extracted path parameters, as the result of the matching, are stored
|
||||
/// into `pathParameters`.
|
||||
static List<RouteMatchBase> match({
|
||||
required RouteBase route,
|
||||
required Map<String, String> pathParameters,
|
||||
required GlobalKey<NavigatorState> rootNavigatorKey,
|
||||
required Uri uri,
|
||||
}) {
|
||||
return _matchByNavigatorKey(
|
||||
route: route,
|
||||
matchedPath: '',
|
||||
remainingLocation: uri.path,
|
||||
matchedLocation: '',
|
||||
pathParameters: pathParameters,
|
||||
scopedNavigatorKey: rootNavigatorKey,
|
||||
uri: uri,
|
||||
)[null] ??
|
||||
const <RouteMatchBase>[];
|
||||
}
|
||||
|
||||
static const Map<GlobalKey<NavigatorState>?, List<RouteMatchBase>> _empty =
|
||||
<GlobalKey<NavigatorState>?, List<RouteMatchBase>>{};
|
||||
|
||||
/// Returns a navigator key to route matches maps.
|
||||
///
|
||||
/// The null key corresponds to the route matches of `scopedNavigatorKey`.
|
||||
/// The scopedNavigatorKey must not be part of the returned map; otherwise,
|
||||
/// it is impossible to order the matches.
|
||||
static Map<GlobalKey<NavigatorState>?, List<RouteMatchBase>>
|
||||
_matchByNavigatorKey({
|
||||
required RouteBase route,
|
||||
required String matchedPath, // e.g. /family/:fid
|
||||
required String remainingLocation, // e.g. person/p1
|
||||
required String matchedLocation, // e.g. /family/f2
|
||||
required Map<String, String> pathParameters,
|
||||
required GlobalKey<NavigatorState> scopedNavigatorKey,
|
||||
required Uri uri,
|
||||
}) {
|
||||
final Map<GlobalKey<NavigatorState>?, List<RouteMatchBase>> result;
|
||||
if (route is ShellRouteBase) {
|
||||
result = _matchByNavigatorKeyForShellRoute(
|
||||
route: route,
|
||||
matchedPath: matchedPath,
|
||||
remainingLocation: remainingLocation,
|
||||
matchedLocation: matchedLocation,
|
||||
pathParameters: pathParameters,
|
||||
scopedNavigatorKey: scopedNavigatorKey,
|
||||
uri: uri,
|
||||
);
|
||||
} else if (route is GoRoute) {
|
||||
result = _matchByNavigatorKeyForGoRoute(
|
||||
route: route,
|
||||
matchedPath: matchedPath,
|
||||
remainingLocation: remainingLocation,
|
||||
matchedLocation: matchedLocation,
|
||||
pathParameters: pathParameters,
|
||||
scopedNavigatorKey: scopedNavigatorKey,
|
||||
uri: uri,
|
||||
);
|
||||
} else {
|
||||
assert(false, 'Unexpected route type: $route');
|
||||
return _empty;
|
||||
}
|
||||
// Grab the route matches for the scope navigator key and put it into the
|
||||
// matches for `null`.
|
||||
if (result.containsKey(scopedNavigatorKey)) {
|
||||
final List<RouteMatchBase> matchesForScopedNavigator =
|
||||
result.remove(scopedNavigatorKey)!;
|
||||
assert(matchesForScopedNavigator.isNotEmpty);
|
||||
result
|
||||
.putIfAbsent(null, () => <RouteMatchBase>[])
|
||||
.addAll(matchesForScopedNavigator);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static Map<GlobalKey<NavigatorState>?, List<RouteMatchBase>>
|
||||
_matchByNavigatorKeyForShellRoute({
|
||||
required ShellRouteBase route,
|
||||
required String matchedPath, // e.g. /family/:fid
|
||||
required String remainingLocation, // e.g. person/p1
|
||||
required String matchedLocation, // e.g. /family/f2
|
||||
required Map<String, String> pathParameters,
|
||||
required GlobalKey<NavigatorState> scopedNavigatorKey,
|
||||
required Uri uri,
|
||||
}) {
|
||||
final GlobalKey<NavigatorState>? parentKey =
|
||||
route.parentNavigatorKey == scopedNavigatorKey
|
||||
? null
|
||||
: route.parentNavigatorKey;
|
||||
Map<GlobalKey<NavigatorState>?, List<RouteMatchBase>>? subRouteMatches;
|
||||
late GlobalKey<NavigatorState> navigatorKeyUsed;
|
||||
for (final RouteBase subRoute in route.routes) {
|
||||
navigatorKeyUsed = route.navigatorKeyForSubRoute(subRoute);
|
||||
subRouteMatches = _matchByNavigatorKey(
|
||||
route: subRoute,
|
||||
matchedPath: matchedPath,
|
||||
remainingLocation: remainingLocation,
|
||||
matchedLocation: matchedLocation,
|
||||
pathParameters: pathParameters,
|
||||
uri: uri,
|
||||
scopedNavigatorKey: navigatorKeyUsed,
|
||||
);
|
||||
assert(!subRouteMatches
|
||||
.containsKey(route.navigatorKeyForSubRoute(subRoute)));
|
||||
if (subRouteMatches.isNotEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (subRouteMatches?.isEmpty ?? true) {
|
||||
return _empty;
|
||||
}
|
||||
final RouteMatchBase result = ShellRouteMatch(
|
||||
route: route,
|
||||
// The RouteConfiguration should have asserted the subRouteMatches must
|
||||
// have at least one match for this ShellRouteBase.
|
||||
matches: subRouteMatches!.remove(null)!,
|
||||
matchedLocation: remainingLocation,
|
||||
pageKey: ValueKey<String>(route.hashCode.toString()),
|
||||
navigatorKey: navigatorKeyUsed,
|
||||
);
|
||||
subRouteMatches.putIfAbsent(parentKey, () => <RouteMatchBase>[]).insert(
|
||||
0,
|
||||
result,
|
||||
);
|
||||
|
||||
return subRouteMatches;
|
||||
}
|
||||
|
||||
static Map<GlobalKey<NavigatorState>?, List<RouteMatchBase>>
|
||||
_matchByNavigatorKeyForGoRoute({
|
||||
required GoRoute route,
|
||||
required String matchedPath, // e.g. /family/:fid
|
||||
required String remainingLocation, // e.g. person/p1
|
||||
required String matchedLocation, // e.g. /family/f2
|
||||
required Map<String, String> pathParameters,
|
||||
required GlobalKey<NavigatorState> scopedNavigatorKey,
|
||||
required Uri uri,
|
||||
}) {
|
||||
final GlobalKey<NavigatorState>? parentKey =
|
||||
route.parentNavigatorKey == scopedNavigatorKey
|
||||
? null
|
||||
: route.parentNavigatorKey;
|
||||
|
||||
final RegExpMatch? regExpMatch =
|
||||
route.matchPatternAsPrefix(remainingLocation);
|
||||
if (regExpMatch == null) {
|
||||
return _empty;
|
||||
}
|
||||
final Map<String, String> encodedParams =
|
||||
route.extractPathParams(regExpMatch);
|
||||
// A temporary map to hold path parameters. This map is merged into
|
||||
// pathParameters only when this route is part of the returned result.
|
||||
final Map<String, String> currentPathParameter =
|
||||
encodedParams.map<String, String>((String key, String value) =>
|
||||
MapEntry<String, String>(key, Uri.decodeComponent(value)));
|
||||
final String pathLoc = patternToPath(route.path, encodedParams);
|
||||
final String newMatchedLocation =
|
||||
concatenatePaths(matchedLocation, pathLoc);
|
||||
final String newMatchedPath = concatenatePaths(matchedPath, route.path);
|
||||
if (newMatchedLocation.toLowerCase() == uri.path.toLowerCase()) {
|
||||
// A complete match.
|
||||
pathParameters.addAll(currentPathParameter);
|
||||
|
||||
return <GlobalKey<NavigatorState>?, List<RouteMatchBase>>{
|
||||
parentKey: <RouteMatchBase>[
|
||||
RouteMatch(
|
||||
route: route,
|
||||
matchedLocation: newMatchedLocation,
|
||||
pageKey: ValueKey<String>(newMatchedPath),
|
||||
),
|
||||
],
|
||||
};
|
||||
}
|
||||
assert(uri.path.startsWith(newMatchedLocation));
|
||||
assert(remainingLocation.isNotEmpty);
|
||||
|
||||
final String childRestLoc = uri.path.substring(
|
||||
newMatchedLocation.length + (newMatchedLocation == '/' ? 0 : 1));
|
||||
|
||||
Map<GlobalKey<NavigatorState>?, List<RouteMatchBase>>? subRouteMatches;
|
||||
for (final RouteBase subRoute in route.routes) {
|
||||
subRouteMatches = _matchByNavigatorKey(
|
||||
route: subRoute,
|
||||
matchedPath: newMatchedPath,
|
||||
remainingLocation: childRestLoc,
|
||||
matchedLocation: newMatchedLocation,
|
||||
pathParameters: pathParameters,
|
||||
uri: uri,
|
||||
scopedNavigatorKey: scopedNavigatorKey,
|
||||
);
|
||||
if (subRouteMatches.isNotEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (subRouteMatches?.isEmpty ?? true) {
|
||||
// If not finding a sub route match, it is considered not matched for this
|
||||
// route even if this route match part of the `remainingLocation`.
|
||||
return _empty;
|
||||
}
|
||||
|
||||
pathParameters.addAll(currentPathParameter);
|
||||
subRouteMatches!.putIfAbsent(parentKey, () => <RouteMatchBase>[]).insert(
|
||||
0,
|
||||
RouteMatch(
|
||||
route: route,
|
||||
matchedLocation: newMatchedLocation,
|
||||
pageKey: ValueKey<String>(newMatchedPath),
|
||||
));
|
||||
return subRouteMatches;
|
||||
}
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<RouteBase>('route', route));
|
||||
}
|
||||
}
|
||||
|
||||
/// An matched result by matching a [GoRoute] against a location.
|
||||
///
|
||||
/// This is typically created by calling [RouteMatchBase.match].
|
||||
@immutable
|
||||
class RouteMatch extends RouteMatchBase {
|
||||
/// Constructor for [RouteMatch].
|
||||
const RouteMatch({
|
||||
required this.route,
|
||||
required this.matchedLocation,
|
||||
required this.pageKey,
|
||||
});
|
||||
|
||||
/// The matched route.
|
||||
@override
|
||||
final GoRoute route;
|
||||
|
||||
@override
|
||||
final String matchedLocation;
|
||||
|
||||
/// Value key of type string, to hold a unique reference to a page.
|
||||
@override
|
||||
final ValueKey<String> pageKey;
|
||||
|
||||
@override
|
||||
@ -102,6 +311,109 @@ class RouteMatch {
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(route, matchedLocation, pageKey);
|
||||
|
||||
@override
|
||||
GoRouterState buildState(
|
||||
RouteConfiguration configuration, RouteMatchList matches) {
|
||||
return GoRouterState(
|
||||
configuration,
|
||||
uri: matches.uri,
|
||||
matchedLocation: matchedLocation,
|
||||
fullPath: matches.fullPath,
|
||||
pathParameters: matches.pathParameters,
|
||||
pageKey: pageKey,
|
||||
name: route.name,
|
||||
path: route.path,
|
||||
extra: matches.extra,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// An matched result by matching a [ShellRoute] against a location.
|
||||
///
|
||||
/// This is typically created by calling [RouteMatchBase.match].
|
||||
@immutable
|
||||
class ShellRouteMatch extends RouteMatchBase {
|
||||
/// Create a match.
|
||||
ShellRouteMatch({
|
||||
required this.route,
|
||||
required this.matches,
|
||||
required this.matchedLocation,
|
||||
required this.pageKey,
|
||||
required this.navigatorKey,
|
||||
}) : assert(matches.isNotEmpty);
|
||||
|
||||
@override
|
||||
final ShellRouteBase route;
|
||||
|
||||
RouteMatch get _lastLeaf {
|
||||
RouteMatchBase currentMatch = matches.last;
|
||||
while (currentMatch is ShellRouteMatch) {
|
||||
currentMatch = currentMatch.matches.last;
|
||||
}
|
||||
return currentMatch as RouteMatch;
|
||||
}
|
||||
|
||||
/// The navigator key used for this match.
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
@override
|
||||
final String matchedLocation;
|
||||
|
||||
/// The matches that will be built under this shell route.
|
||||
final List<RouteMatchBase> matches;
|
||||
|
||||
@override
|
||||
final ValueKey<String> pageKey;
|
||||
|
||||
@override
|
||||
GoRouterState buildState(
|
||||
RouteConfiguration configuration, RouteMatchList matches) {
|
||||
// The route related data is stored in the leaf route match.
|
||||
final RouteMatch leafMatch = _lastLeaf;
|
||||
if (leafMatch is ImperativeRouteMatch) {
|
||||
matches = leafMatch.matches;
|
||||
}
|
||||
return GoRouterState(
|
||||
configuration,
|
||||
uri: matches.uri,
|
||||
matchedLocation: matchedLocation,
|
||||
fullPath: matches.fullPath,
|
||||
pathParameters: matches.pathParameters,
|
||||
pageKey: pageKey,
|
||||
extra: matches.extra,
|
||||
);
|
||||
}
|
||||
|
||||
/// Creates a new shell route match with the given matches.
|
||||
///
|
||||
/// This is typically used when pushing or popping [RouteMatchBase] from
|
||||
/// [RouteMatchList].
|
||||
@internal
|
||||
ShellRouteMatch copyWith({
|
||||
required List<RouteMatchBase>? matches,
|
||||
}) {
|
||||
return ShellRouteMatch(
|
||||
matches: matches ?? this.matches,
|
||||
route: route,
|
||||
matchedLocation: matchedLocation,
|
||||
pageKey: pageKey,
|
||||
navigatorKey: navigatorKey,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ShellRouteMatch &&
|
||||
route == other.route &&
|
||||
matchedLocation == other.matchedLocation &&
|
||||
const ListEquality<RouteMatchBase>().equals(matches, other.matches) &&
|
||||
pageKey == other.pageKey;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode =>
|
||||
Object.hash(route, matchedLocation, Object.hashAll(matches), pageKey);
|
||||
}
|
||||
|
||||
/// The route match that represent route pushed through [GoRouter.push].
|
||||
@ -113,7 +425,8 @@ class ImperativeRouteMatch extends RouteMatch {
|
||||
route: _getsLastRouteFromMatches(matches),
|
||||
matchedLocation: _getsMatchedLocationFromMatches(matches),
|
||||
);
|
||||
static RouteBase _getsLastRouteFromMatches(RouteMatchList matchList) {
|
||||
|
||||
static GoRoute _getsLastRouteFromMatches(RouteMatchList matchList) {
|
||||
if (matchList.isError) {
|
||||
return GoRoute(
|
||||
path: 'error', builder: (_, __) => throw UnimplementedError());
|
||||
@ -140,6 +453,12 @@ class ImperativeRouteMatch extends RouteMatch {
|
||||
completer.complete(value);
|
||||
}
|
||||
|
||||
@override
|
||||
GoRouterState buildState(
|
||||
RouteConfiguration configuration, RouteMatchList matches) {
|
||||
return super.buildState(configuration, this.matches);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ImperativeRouteMatch &&
|
||||
@ -152,11 +471,13 @@ class ImperativeRouteMatch extends RouteMatch {
|
||||
int get hashCode => Object.hash(super.hashCode, completer, matches.hashCode);
|
||||
}
|
||||
|
||||
/// The list of [RouteMatch] objects.
|
||||
/// The list of [RouteMatchBase] objects.
|
||||
///
|
||||
/// This can contains tree structure if there are [ShellRouteMatch] in the list.
|
||||
///
|
||||
/// This corresponds to the GoRouter's history.
|
||||
@immutable
|
||||
class RouteMatchList {
|
||||
class RouteMatchList with Diagnosticable {
|
||||
/// RouteMatchList constructor.
|
||||
RouteMatchList({
|
||||
required this.matches,
|
||||
@ -168,12 +489,12 @@ class RouteMatchList {
|
||||
|
||||
/// Constructs an empty matches object.
|
||||
static RouteMatchList empty = RouteMatchList(
|
||||
matches: const <RouteMatch>[],
|
||||
matches: const <RouteMatchBase>[],
|
||||
uri: Uri(),
|
||||
pathParameters: const <String, String>{});
|
||||
|
||||
/// The route matches.
|
||||
final List<RouteMatch> matches;
|
||||
final List<RouteMatchBase> matches;
|
||||
|
||||
/// Parameters for the matched route, URI-encoded.
|
||||
///
|
||||
@ -231,19 +552,25 @@ class RouteMatchList {
|
||||
/// ```dart
|
||||
/// [RouteMatchA(), RouteMatchB(), RouteMatchC()]
|
||||
/// ```
|
||||
static String _generateFullPath(Iterable<RouteMatch> matches) {
|
||||
static String _generateFullPath(Iterable<RouteMatchBase> matches) {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
bool addsSlash = false;
|
||||
for (final RouteMatch match in matches
|
||||
.where((RouteMatch match) => match is! ImperativeRouteMatch)) {
|
||||
final RouteBase route = match.route;
|
||||
if (route is GoRoute) {
|
||||
if (addsSlash) {
|
||||
buffer.write('/');
|
||||
}
|
||||
buffer.write(route.path);
|
||||
addsSlash = addsSlash || route.path != '/';
|
||||
for (final RouteMatchBase match in matches
|
||||
.where((RouteMatchBase match) => match is! ImperativeRouteMatch)) {
|
||||
if (addsSlash) {
|
||||
buffer.write('/');
|
||||
}
|
||||
final String pathSegment;
|
||||
if (match is RouteMatch) {
|
||||
pathSegment = match.route.path;
|
||||
} else if (match is ShellRouteMatch) {
|
||||
pathSegment = _generateFullPath(match.matches);
|
||||
} else {
|
||||
assert(false, 'Unexpected match type: $match');
|
||||
continue;
|
||||
}
|
||||
buffer.write(pathSegment);
|
||||
addsSlash = pathSegment.isNotEmpty && (addsSlash || pathSegment != '/');
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
@ -257,32 +584,74 @@ class RouteMatchList {
|
||||
/// Returns a new instance of RouteMatchList with the input `match` pushed
|
||||
/// onto the current instance.
|
||||
RouteMatchList push(ImperativeRouteMatch match) {
|
||||
// Imperative route match doesn't change the uri and path parameters.
|
||||
return _copyWith(matches: <RouteMatch>[...matches, match]);
|
||||
if (match.matches.isError) {
|
||||
return copyWith(matches: <RouteMatchBase>[...matches, match]);
|
||||
}
|
||||
return copyWith(
|
||||
matches: _createNewMatchUntilIncompatible(
|
||||
matches,
|
||||
match.matches.matches,
|
||||
match,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static List<RouteMatchBase> _createNewMatchUntilIncompatible(
|
||||
List<RouteMatchBase> currentMatches,
|
||||
List<RouteMatchBase> otherMatches,
|
||||
ImperativeRouteMatch match,
|
||||
) {
|
||||
final List<RouteMatchBase> newMatches = currentMatches.toList();
|
||||
if (otherMatches.last is ShellRouteMatch &&
|
||||
newMatches.isNotEmpty &&
|
||||
otherMatches.last.route == newMatches.last.route) {
|
||||
assert(newMatches.last is ShellRouteMatch);
|
||||
final ShellRouteMatch lastShellRouteMatch =
|
||||
newMatches.removeLast() as ShellRouteMatch;
|
||||
newMatches.add(
|
||||
// Create a new copy of the `lastShellRouteMatch`.
|
||||
lastShellRouteMatch.copyWith(
|
||||
matches: _createNewMatchUntilIncompatible(lastShellRouteMatch.matches,
|
||||
(otherMatches.last as ShellRouteMatch).matches, match),
|
||||
),
|
||||
);
|
||||
return newMatches;
|
||||
}
|
||||
newMatches
|
||||
.add(_cloneBranchAndInsertImperativeMatch(otherMatches.last, match));
|
||||
return newMatches;
|
||||
}
|
||||
|
||||
static RouteMatchBase _cloneBranchAndInsertImperativeMatch(
|
||||
RouteMatchBase branch, ImperativeRouteMatch match) {
|
||||
if (branch is ShellRouteMatch) {
|
||||
return branch.copyWith(
|
||||
matches: <RouteMatchBase>[
|
||||
_cloneBranchAndInsertImperativeMatch(branch.matches.last, match),
|
||||
],
|
||||
);
|
||||
}
|
||||
// Add the input `match` instead of the incompatibleMatch since it contains
|
||||
// page key and push future.
|
||||
assert(branch.route == match.route);
|
||||
return match;
|
||||
}
|
||||
|
||||
/// Returns a new instance of RouteMatchList with the input `match` removed
|
||||
/// from the current instance.
|
||||
RouteMatchList remove(RouteMatch match) {
|
||||
final List<RouteMatch> newMatches = matches.toList();
|
||||
final int index = newMatches.indexOf(match);
|
||||
assert(index != -1);
|
||||
newMatches.removeRange(index, newMatches.length);
|
||||
|
||||
// Also pop ShellRoutes that have no subsequent route matches and GoRoutes
|
||||
// that only have redirect.
|
||||
while (newMatches.isNotEmpty &&
|
||||
(newMatches.last.route is ShellRouteBase ||
|
||||
(newMatches.last.route as GoRoute).redirectOnly)) {
|
||||
newMatches.removeLast();
|
||||
}
|
||||
// Removing ImperativeRouteMatch should not change uri and pathParameters.
|
||||
if (match is ImperativeRouteMatch) {
|
||||
return _copyWith(matches: newMatches);
|
||||
RouteMatchList remove(RouteMatchBase match) {
|
||||
final List<RouteMatchBase> newMatches =
|
||||
_removeRouteMatchFromList(matches, match);
|
||||
if (newMatches == matches) {
|
||||
return this;
|
||||
}
|
||||
|
||||
final String fullPath = _generateFullPath(
|
||||
newMatches.where((RouteMatch match) => match is! ImperativeRouteMatch));
|
||||
final String fullPath = _generateFullPath(newMatches);
|
||||
if (this.fullPath == fullPath) {
|
||||
return copyWith(
|
||||
matches: newMatches,
|
||||
);
|
||||
}
|
||||
// Need to remove path parameters that are no longer in the fullPath.
|
||||
final List<String> newParameters = <String>[];
|
||||
patternToRegExp(fullPath, newParameters);
|
||||
@ -294,24 +663,110 @@ class RouteMatchList {
|
||||
);
|
||||
final Uri newUri =
|
||||
uri.replace(path: patternToPath(fullPath, newPathParameters));
|
||||
return _copyWith(
|
||||
return copyWith(
|
||||
matches: newMatches,
|
||||
uri: newUri,
|
||||
pathParameters: newPathParameters,
|
||||
);
|
||||
}
|
||||
|
||||
/// The last matching route.
|
||||
RouteMatch get last => matches.last;
|
||||
/// Returns a new List from the input matches with target removed.
|
||||
///
|
||||
/// This method recursively looks into any ShellRouteMatch in matches and
|
||||
/// removes target if it found a match in the match list nested in
|
||||
/// ShellRouteMatch.
|
||||
///
|
||||
/// This method returns a new list as long as the target is found in the
|
||||
/// matches' subtree.
|
||||
///
|
||||
/// If a target is found, the target and every node after the target in tree
|
||||
/// order is removed.
|
||||
static List<RouteMatchBase> _removeRouteMatchFromList(
|
||||
List<RouteMatchBase> matches, RouteMatchBase target) {
|
||||
// Remove is caused by pop; therefore, start searching from the end.
|
||||
for (int index = matches.length - 1; index >= 0; index -= 1) {
|
||||
final RouteMatchBase match = matches[index];
|
||||
if (match == target) {
|
||||
// Remove any redirect only route immediately before the target.
|
||||
while (index > 0) {
|
||||
final RouteMatchBase lookBefore = matches[index - 1];
|
||||
if (lookBefore is! RouteMatch || !lookBefore.route.redirectOnly) {
|
||||
break;
|
||||
}
|
||||
index -= 1;
|
||||
}
|
||||
return matches.sublist(0, index);
|
||||
}
|
||||
if (match is ShellRouteMatch) {
|
||||
final List<RouteMatchBase> newSubMatches =
|
||||
_removeRouteMatchFromList(match.matches, target);
|
||||
if (newSubMatches == match.matches) {
|
||||
// Didn't find target in the newSubMatches.
|
||||
continue;
|
||||
}
|
||||
// Removes `match` if its sub match list become empty after the remove.
|
||||
return <RouteMatchBase>[
|
||||
...matches.sublist(0, index),
|
||||
if (newSubMatches.isNotEmpty) match.copyWith(matches: newSubMatches),
|
||||
];
|
||||
}
|
||||
}
|
||||
// Target is not in the match subtree.
|
||||
return matches;
|
||||
}
|
||||
|
||||
/// The last leaf route.
|
||||
///
|
||||
/// If the last RouteMatchBase from [matches] is a ShellRouteMatch, it
|
||||
/// recursively goes into its [ShellRouteMatch.matches] until it reach the leaf
|
||||
/// [RouteMatch].
|
||||
RouteMatch get last {
|
||||
if (matches.last is RouteMatch) {
|
||||
return matches.last as RouteMatch;
|
||||
}
|
||||
return (matches.last as ShellRouteMatch)._lastLeaf;
|
||||
}
|
||||
|
||||
/// Returns true if the current match intends to display an error screen.
|
||||
bool get isError => error != null;
|
||||
|
||||
/// The routes for each of the matches.
|
||||
List<RouteBase> get routes => matches.map((RouteMatch e) => e.route).toList();
|
||||
List<RouteBase> get routes {
|
||||
final List<RouteBase> result = <RouteBase>[];
|
||||
visitRouteMatches((RouteMatchBase match) {
|
||||
result.add(match.route);
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
RouteMatchList _copyWith({
|
||||
List<RouteMatch>? matches,
|
||||
/// Traverse route matches in this match list in preorder until visitor
|
||||
/// returns false.
|
||||
///
|
||||
/// This method visit recursively into shell route matches.
|
||||
@internal
|
||||
void visitRouteMatches(RouteMatchVisitor visitor) {
|
||||
_visitRouteMatches(matches, visitor);
|
||||
}
|
||||
|
||||
static bool _visitRouteMatches(
|
||||
List<RouteMatchBase> matches, RouteMatchVisitor visitor) {
|
||||
for (final RouteMatchBase routeMatch in matches) {
|
||||
if (!visitor(routeMatch)) {
|
||||
return false;
|
||||
}
|
||||
if (routeMatch is ShellRouteMatch &&
|
||||
!_visitRouteMatches(routeMatch.matches, visitor)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Create a new [RouteMatchList] with given parameter replaced.
|
||||
@internal
|
||||
RouteMatchList copyWith({
|
||||
List<RouteMatchBase>? matches,
|
||||
Uri? uri,
|
||||
Map<String, String>? pathParameters,
|
||||
}) {
|
||||
@ -332,7 +787,7 @@ class RouteMatchList {
|
||||
uri == other.uri &&
|
||||
extra == other.extra &&
|
||||
error == other.error &&
|
||||
const ListEquality<RouteMatch>().equals(matches, other.matches) &&
|
||||
const ListEquality<RouteMatchBase>().equals(matches, other.matches) &&
|
||||
const MapEquality<String, String>()
|
||||
.equals(pathParameters, other.pathParameters);
|
||||
}
|
||||
@ -352,8 +807,11 @@ class RouteMatchList {
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${objectRuntimeType(this, 'RouteMatchList')}($fullPath)';
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.add(DiagnosticsProperty<Uri>('uri', uri));
|
||||
properties
|
||||
.add(DiagnosticsProperty<List<RouteMatchBase>>('matches', matches));
|
||||
}
|
||||
}
|
||||
|
||||
@ -391,15 +849,23 @@ class _RouteMatchListEncoder
|
||||
final RouteConfiguration configuration;
|
||||
@override
|
||||
Map<Object?, Object?> convert(RouteMatchList input) {
|
||||
final List<Map<Object?, Object?>> imperativeMatches = input.matches
|
||||
.whereType<ImperativeRouteMatch>()
|
||||
.map((ImperativeRouteMatch e) => _toPrimitives(
|
||||
e.matches.uri.toString(), e.matches.extra,
|
||||
pageKey: e.pageKey.value))
|
||||
.toList();
|
||||
final List<ImperativeRouteMatch> imperativeMatches =
|
||||
<ImperativeRouteMatch>[];
|
||||
input.visitRouteMatches((RouteMatchBase match) {
|
||||
if (match is ImperativeRouteMatch) {
|
||||
imperativeMatches.add(match);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
final List<Map<Object?, Object?>> encodedImperativeMatches =
|
||||
imperativeMatches
|
||||
.map((ImperativeRouteMatch e) => _toPrimitives(
|
||||
e.matches.uri.toString(), e.matches.extra,
|
||||
pageKey: e.pageKey.value))
|
||||
.toList();
|
||||
|
||||
return _toPrimitives(input.uri.toString(), input.extra,
|
||||
imperativeMatches: imperativeMatches);
|
||||
imperativeMatches: encodedImperativeMatches);
|
||||
}
|
||||
|
||||
Map<Object?, Object?> _toPrimitives(String location, Object? extra,
|
||||
|
@ -8,8 +8,8 @@ import 'package:flutter/cupertino.dart';
|
||||
import '../misc/extensions.dart';
|
||||
|
||||
/// Checks for CupertinoApp in the widget tree.
|
||||
bool isCupertinoApp(Element elem) =>
|
||||
elem.findAncestorWidgetOfExactType<CupertinoApp>() != null;
|
||||
bool isCupertinoApp(BuildContext context) =>
|
||||
context.findAncestorWidgetOfExactType<CupertinoApp>() != null;
|
||||
|
||||
/// Creates a Cupertino HeroController.
|
||||
HeroController createCupertinoHeroController() =>
|
||||
|
@ -9,8 +9,8 @@ import 'package:flutter/material.dart';
|
||||
import '../misc/extensions.dart';
|
||||
|
||||
/// Checks for MaterialApp in the widget tree.
|
||||
bool isMaterialApp(Element elem) =>
|
||||
elem.findAncestorWidgetOfExactType<MaterialApp>() != null;
|
||||
bool isMaterialApp(BuildContext context) =>
|
||||
context.findAncestorWidgetOfExactType<MaterialApp>() != null;
|
||||
|
||||
/// Creates a Material HeroController.
|
||||
HeroController createMaterialHeroController() =>
|
||||
|
@ -12,7 +12,6 @@ import 'configuration.dart';
|
||||
import 'information_provider.dart';
|
||||
import 'logging.dart';
|
||||
import 'match.dart';
|
||||
import 'route.dart';
|
||||
import 'router.dart';
|
||||
|
||||
/// The function signature of [GoRouteInformationParser.onParserException].
|
||||
@ -105,7 +104,7 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
|
||||
|
||||
assert(() {
|
||||
if (matchList.isNotEmpty) {
|
||||
assert(!(matchList.last.route as GoRoute).redirectOnly,
|
||||
assert(!matchList.last.route.redirectOnly,
|
||||
'A redirect-only route must redirect to location different from itself.\n The offending route: ${matchList.last.route}');
|
||||
}
|
||||
return true;
|
||||
|
@ -493,18 +493,6 @@ abstract class ShellRouteBase extends RouteBase {
|
||||
/// Returns the key for the [Navigator] that is to be used for the specified
|
||||
/// immediate sub-route of this shell route.
|
||||
GlobalKey<NavigatorState> navigatorKeyForSubRoute(RouteBase subRoute);
|
||||
|
||||
/// Returns the keys for the [Navigator]s associated with this shell route.
|
||||
Iterable<GlobalKey<NavigatorState>> get _navigatorKeys =>
|
||||
<GlobalKey<NavigatorState>>[];
|
||||
|
||||
/// Returns all the Navigator keys of this shell route as well as those of any
|
||||
/// descendant shell routes.
|
||||
Iterable<GlobalKey<NavigatorState>> _navigatorKeysRecursively() {
|
||||
return RouteBase.routesRecursively(<ShellRouteBase>[this])
|
||||
.whereType<ShellRouteBase>()
|
||||
.expand((ShellRouteBase e) => e._navigatorKeys);
|
||||
}
|
||||
}
|
||||
|
||||
/// Context object used when building the shell and Navigator for a shell route.
|
||||
@ -710,10 +698,6 @@ class ShellRoute extends ShellRouteBase {
|
||||
return navigatorKey;
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<GlobalKey<NavigatorState>> get _navigatorKeys =>
|
||||
<GlobalKey<NavigatorState>>[navigatorKey];
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
@ -920,7 +904,6 @@ class StatefulShellRoute extends ShellRouteBase {
|
||||
return branch!.navigatorKey;
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<GlobalKey<NavigatorState>> get _navigatorKeys =>
|
||||
branches.map((StatefulShellBranch b) => b.navigatorKey);
|
||||
|
||||
@ -1222,26 +1205,24 @@ class StatefulNavigationShellState extends State<StatefulNavigationShell>
|
||||
/// trailing imperative matches from the RouterMatchList that are targeted at
|
||||
/// any other (often top-level) Navigator.
|
||||
RouteMatchList _scopedMatchList(RouteMatchList matchList) {
|
||||
final Iterable<GlobalKey<NavigatorState>> validKeys =
|
||||
route._navigatorKeysRecursively();
|
||||
final int index = matchList.matches.indexWhere((RouteMatch e) {
|
||||
final RouteBase route = e.route;
|
||||
if (e is ImperativeRouteMatch && route is GoRoute) {
|
||||
return route.parentNavigatorKey != null &&
|
||||
!validKeys.contains(route.parentNavigatorKey);
|
||||
return matchList.copyWith(matches: _scopeMatches(matchList.matches));
|
||||
}
|
||||
|
||||
List<RouteMatchBase> _scopeMatches(List<RouteMatchBase> matches) {
|
||||
final List<RouteMatchBase> result = <RouteMatchBase>[];
|
||||
for (final RouteMatchBase match in matches) {
|
||||
if (match is ShellRouteMatch) {
|
||||
if (match.route == route) {
|
||||
result.add(match);
|
||||
// Discard any other route match after current shell route.
|
||||
break;
|
||||
}
|
||||
result.add(match.copyWith(matches: _scopeMatches(match.matches)));
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (index > 0) {
|
||||
final List<RouteMatch> matches = matchList.matches.sublist(0, index);
|
||||
return RouteMatchList(
|
||||
extra: matchList.extra,
|
||||
matches: matches,
|
||||
uri: Uri.parse(matches.last.matchedLocation),
|
||||
pathParameters: matchList.pathParameters,
|
||||
);
|
||||
result.add(match);
|
||||
}
|
||||
return matchList;
|
||||
return result;
|
||||
}
|
||||
|
||||
void _updateCurrentBranchStateFromWidget() {
|
||||
|
@ -1,7 +1,7 @@
|
||||
name: go_router
|
||||
description: A declarative router for Flutter based on Navigation 2 supporting
|
||||
deep linking, data-driven routes and more
|
||||
version: 12.1.3
|
||||
version: 13.0.0
|
||||
repository: https://github.com/flutter/packages/tree/main/packages/go_router
|
||||
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
|
||||
|
||||
|
@ -49,21 +49,24 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('Builds ShellRoute', (WidgetTester tester) async {
|
||||
final GlobalKey<NavigatorState> shellNavigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
final RouteConfiguration config = createRouteConfiguration(
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
builder:
|
||||
(BuildContext context, GoRouterState state, Widget child) {
|
||||
return _DetailsScreen();
|
||||
},
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return _DetailsScreen();
|
||||
},
|
||||
),
|
||||
]),
|
||||
navigatorKey: shellNavigatorKey,
|
||||
builder: (BuildContext context, GoRouterState state, Widget child) {
|
||||
return _DetailsScreen();
|
||||
},
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, GoRouterState state) {
|
||||
return _DetailsScreen();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
redirectLimit: 10,
|
||||
topRedirect: (BuildContext context, GoRouterState state) {
|
||||
@ -73,9 +76,20 @@ void main() {
|
||||
);
|
||||
|
||||
final RouteMatchList matches = RouteMatchList(
|
||||
matches: <RouteMatch>[
|
||||
createRouteMatch(config.routes.first, '/'),
|
||||
createRouteMatch(config.routes.first.routes.first, '/'),
|
||||
matches: <RouteMatchBase>[
|
||||
ShellRouteMatch(
|
||||
route: config.routes.first as ShellRouteBase,
|
||||
matchedLocation: '',
|
||||
pageKey: const ValueKey<String>(''),
|
||||
navigatorKey: shellNavigatorKey,
|
||||
matches: <RouteMatchBase>[
|
||||
RouteMatch(
|
||||
route: config.routes.first.routes.first as GoRoute,
|
||||
matchedLocation: '/',
|
||||
pageKey: const ValueKey<String>('/'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
uri: Uri.parse('/'),
|
||||
pathParameters: const <String, String>{},
|
||||
@ -164,17 +178,19 @@ void main() {
|
||||
);
|
||||
|
||||
final RouteMatchList matches = RouteMatchList(
|
||||
matches: <RouteMatch>[
|
||||
RouteMatch(
|
||||
route: config.routes.first,
|
||||
matchedLocation: '',
|
||||
pageKey: const ValueKey<String>(''),
|
||||
),
|
||||
RouteMatch(
|
||||
route: config.routes.first.routes.first,
|
||||
matchedLocation: '/details',
|
||||
pageKey: const ValueKey<String>('/details'),
|
||||
),
|
||||
matches: <RouteMatchBase>[
|
||||
ShellRouteMatch(
|
||||
route: config.routes.first as ShellRouteBase,
|
||||
matchedLocation: '',
|
||||
pageKey: const ValueKey<String>(''),
|
||||
navigatorKey: shellNavigatorKey,
|
||||
matches: <RouteMatchBase>[
|
||||
RouteMatch(
|
||||
route: config.routes.first.routes.first as GoRoute,
|
||||
matchedLocation: '/details',
|
||||
pageKey: const ValueKey<String>('/details'),
|
||||
),
|
||||
]),
|
||||
],
|
||||
uri: Uri.parse('/details'),
|
||||
pathParameters: const <String, String>{});
|
||||
@ -290,9 +306,20 @@ void main() {
|
||||
);
|
||||
|
||||
final RouteMatchList matches = RouteMatchList(
|
||||
matches: <RouteMatch>[
|
||||
createRouteMatch(config.routes.first, ''),
|
||||
createRouteMatch(config.routes.first.routes.first, '/a'),
|
||||
matches: <RouteMatchBase>[
|
||||
ShellRouteMatch(
|
||||
route: config.routes.first as ShellRouteBase,
|
||||
matchedLocation: '',
|
||||
pageKey: const ValueKey<String>(''),
|
||||
navigatorKey: shellNavigatorKey,
|
||||
matches: <RouteMatchBase>[
|
||||
RouteMatch(
|
||||
route: config.routes.first.routes.first as GoRoute,
|
||||
matchedLocation: '/a',
|
||||
pageKey: const ValueKey<String>('/a'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
uri: Uri.parse('/b'),
|
||||
pathParameters: const <String, String>{},
|
||||
@ -430,8 +457,7 @@ class _BuilderTestWidget extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
home: builder.tryBuild(context, matches, false,
|
||||
routeConfiguration.navigatorKey, <Page<Object?>, GoRouterState>{}),
|
||||
home: builder.build(context, matches, false),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -77,8 +77,8 @@ void main() {
|
||||
final GoRouter goRouter = await createGoRouter(tester)
|
||||
..push('/error');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final RouteMatch last =
|
||||
expect(find.byType(ErrorScreen), findsOneWidget);
|
||||
final RouteMatchBase last =
|
||||
goRouter.routerDelegate.currentConfiguration.matches.last;
|
||||
await goRouter.routerDelegate.popRoute();
|
||||
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 1);
|
||||
@ -194,10 +194,8 @@ void main() {
|
||||
await createGoRouterWithStatefulShellRoute(tester);
|
||||
goRouter.push('/c/c1');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
goRouter.push('/a');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
|
||||
expect(
|
||||
goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
|
||||
@ -219,11 +217,13 @@ void main() {
|
||||
goRouter.push('/c/c2');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
|
||||
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
|
||||
final ShellRouteMatch shellRouteMatch = goRouter.routerDelegate
|
||||
.currentConfiguration.matches.last as ShellRouteMatch;
|
||||
expect(shellRouteMatch.matches.length, 2);
|
||||
expect(
|
||||
goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
|
||||
isNot(equals(
|
||||
goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
|
||||
shellRouteMatch.matches[0].pageKey,
|
||||
isNot(equals(shellRouteMatch.matches[1].pageKey)),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -240,11 +240,13 @@ void main() {
|
||||
goRouter.push('/c');
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 3);
|
||||
expect(goRouter.routerDelegate.currentConfiguration.matches.length, 2);
|
||||
final ShellRouteMatch shellRouteMatch = goRouter.routerDelegate
|
||||
.currentConfiguration.matches.last as ShellRouteMatch;
|
||||
expect(shellRouteMatch.matches.length, 2);
|
||||
expect(
|
||||
goRouter.routerDelegate.currentConfiguration.matches[1].pageKey,
|
||||
isNot(equals(
|
||||
goRouter.routerDelegate.currentConfiguration.matches[2].pageKey)),
|
||||
shellRouteMatch.matches[0].pageKey,
|
||||
isNot(equals(shellRouteMatch.matches[1].pageKey)),
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -294,7 +296,7 @@ void main() {
|
||||
goRouter.push('/page-0');
|
||||
|
||||
goRouter.routerDelegate.addListener(expectAsync0(() {}));
|
||||
final RouteMatch first =
|
||||
final RouteMatchBase first =
|
||||
goRouter.routerDelegate.currentConfiguration.matches.first;
|
||||
final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
|
||||
goRouter.pushReplacement('/page-1');
|
||||
@ -376,7 +378,7 @@ void main() {
|
||||
goRouter.pushNamed('page0');
|
||||
|
||||
goRouter.routerDelegate.addListener(expectAsync0(() {}));
|
||||
final RouteMatch first =
|
||||
final RouteMatchBase first =
|
||||
goRouter.routerDelegate.currentConfiguration.matches.first;
|
||||
final RouteMatch last =
|
||||
goRouter.routerDelegate.currentConfiguration.last;
|
||||
@ -395,7 +397,7 @@ void main() {
|
||||
expect(
|
||||
goRouter.routerDelegate.currentConfiguration.last,
|
||||
isA<RouteMatch>().having(
|
||||
(RouteMatch match) => (match.route as GoRoute).name,
|
||||
(RouteMatch match) => match.route.name,
|
||||
'match.route.name',
|
||||
'page1',
|
||||
),
|
||||
@ -425,7 +427,7 @@ void main() {
|
||||
goRouter.push('/page-0');
|
||||
|
||||
goRouter.routerDelegate.addListener(expectAsync0(() {}));
|
||||
final RouteMatch first =
|
||||
final RouteMatchBase first =
|
||||
goRouter.routerDelegate.currentConfiguration.matches.first;
|
||||
final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
|
||||
goRouter.replace('/page-1');
|
||||
@ -546,7 +548,7 @@ void main() {
|
||||
goRouter.pushNamed('page0');
|
||||
|
||||
goRouter.routerDelegate.addListener(expectAsync0(() {}));
|
||||
final RouteMatch first =
|
||||
final RouteMatchBase first =
|
||||
goRouter.routerDelegate.currentConfiguration.matches.first;
|
||||
final RouteMatch last = goRouter.routerDelegate.currentConfiguration.last;
|
||||
goRouter.replaceNamed('page1');
|
||||
|
@ -61,7 +61,7 @@ void main() {
|
||||
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/');
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect((matches.first.route as GoRoute).name, '1');
|
||||
@ -135,7 +135,7 @@ void main() {
|
||||
);
|
||||
router.go('/foo');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(0));
|
||||
expect(find.byType(TestErrorScreen), findsOneWidget);
|
||||
@ -156,7 +156,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/login');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/login');
|
||||
@ -186,7 +186,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/login');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/login');
|
||||
@ -211,7 +211,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/login/');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/login');
|
||||
@ -231,7 +231,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/profile/');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/profile/foo');
|
||||
@ -251,7 +251,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/profile/?bar=baz');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(matches.first.matchedLocation, '/profile/foo');
|
||||
@ -348,7 +348,7 @@ void main() {
|
||||
router.pop();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches.length, 4);
|
||||
expect(find.byType(HomeScreen), findsNothing);
|
||||
@ -375,7 +375,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/login');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches.length, 2);
|
||||
expect(matches.first.matchedLocation, '/');
|
||||
@ -497,7 +497,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/bar');
|
||||
await tester.pumpAndSettle();
|
||||
List<RouteMatch> matches =
|
||||
List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(2));
|
||||
expect(find.byType(Page1Screen), findsOneWidget);
|
||||
@ -621,7 +621,7 @@ void main() {
|
||||
const String loc = '/FaMiLy/f2';
|
||||
router.go(loc);
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
|
||||
// NOTE: match the lower case, since location is canonicalized to match the
|
||||
@ -650,7 +650,7 @@ void main() {
|
||||
final GoRouter router = await createRouter(routes, tester);
|
||||
router.go('/user');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(find.byType(DummyScreen), findsOneWidget);
|
||||
@ -908,7 +908,6 @@ void main() {
|
||||
);
|
||||
|
||||
expect(find.text(notifier.value), findsOneWidget);
|
||||
|
||||
notifier.value = 'updated';
|
||||
await tester.pump();
|
||||
expect(find.text(notifier.value), findsOneWidget);
|
||||
@ -1927,7 +1926,7 @@ void main() {
|
||||
errorBuilder: (BuildContext context, GoRouterState state) =>
|
||||
TestErrorScreen(state.error!),
|
||||
);
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(0));
|
||||
expect(find.byType(TestErrorScreen), findsOneWidget);
|
||||
@ -1955,7 +1954,7 @@ void main() {
|
||||
TestErrorScreen(state.error!),
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(0));
|
||||
expect(find.byType(TestErrorScreen), findsOneWidget);
|
||||
@ -1980,7 +1979,7 @@ void main() {
|
||||
TestErrorScreen(state.error!),
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(0));
|
||||
expect(find.byType(TestErrorScreen), findsOneWidget);
|
||||
@ -2004,7 +2003,7 @@ void main() {
|
||||
TestErrorScreen(state.error!),
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(0));
|
||||
expect(find.byType(TestErrorScreen), findsOneWidget);
|
||||
@ -2066,7 +2065,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(find.byType(LoginScreen), findsOneWidget);
|
||||
@ -2101,7 +2100,7 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(2));
|
||||
});
|
||||
@ -2130,7 +2129,7 @@ void main() {
|
||||
initialLocation: loc,
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(1));
|
||||
expect(find.byType(HomeScreen), findsOneWidget);
|
||||
@ -2172,7 +2171,7 @@ void main() {
|
||||
initialLocation: '/family/f2/person/p1',
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches.length, 3);
|
||||
expect(find.byType(HomeScreen, skipOffstage: false), findsOneWidget);
|
||||
@ -2194,7 +2193,7 @@ void main() {
|
||||
redirectLimit: 10,
|
||||
);
|
||||
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
expect(matches, hasLength(0));
|
||||
expect(find.byType(TestErrorScreen), findsOneWidget);
|
||||
@ -2878,7 +2877,10 @@ void main() {
|
||||
RouteMatchList matches = router.routerDelegate.currentConfiguration;
|
||||
|
||||
expect(router.routerDelegate.currentConfiguration.uri.toString(), loc);
|
||||
expect(matches.matches, hasLength(4));
|
||||
expect(matches.matches, hasLength(1));
|
||||
final ShellRouteMatch shellRouteMatch =
|
||||
matches.matches.first as ShellRouteMatch;
|
||||
expect(shellRouteMatch.matches, hasLength(3));
|
||||
expect(find.byType(PersonScreen), findsOneWidget);
|
||||
expect(matches.pathParameters['fid'], fid);
|
||||
expect(matches.pathParameters['pid'], pid);
|
||||
@ -2981,7 +2983,7 @@ void main() {
|
||||
'q2': <String>['v2', 'v3'],
|
||||
});
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
|
||||
expect(matches, hasLength(1));
|
||||
@ -3033,7 +3035,7 @@ void main() {
|
||||
|
||||
router.go('/page?q1=v1&q2=v2&q2=v3');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
|
||||
expect(matches, hasLength(1));
|
||||
@ -3084,7 +3086,7 @@ void main() {
|
||||
|
||||
router.go('/page?q1=v1&q2=v2&q2=v3');
|
||||
await tester.pumpAndSettle();
|
||||
final List<RouteMatch> matches =
|
||||
final List<RouteMatchBase> matches =
|
||||
router.routerDelegate.currentConfiguration.matches;
|
||||
|
||||
expect(matches, hasLength(1));
|
||||
@ -3974,6 +3976,7 @@ void main() {
|
||||
expect(find.text('Screen A'), findsNothing);
|
||||
expect(find.text('Screen B'), findsOneWidget);
|
||||
|
||||
// This push '/common' on top of entire stateful shell route page.
|
||||
router.push('/common', extra: 'X');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Screen A'), findsNothing);
|
||||
@ -3987,8 +3990,7 @@ void main() {
|
||||
routeState!.goBranch(1);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Screen A'), findsNothing);
|
||||
expect(find.text('Screen B'), findsNothing);
|
||||
expect(find.text('Common - X'), findsOneWidget);
|
||||
expect(find.text('Screen B'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
|
297
packages/go_router/test/imperative_api_test.dart
Normal file
297
packages/go_router/test/imperative_api_test.dart
Normal file
@ -0,0 +1,297 @@
|
||||
// Copyright 2013 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_test/flutter_test.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import 'test_helpers.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('replace inside shell route', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/134524.
|
||||
final UniqueKey a = UniqueKey();
|
||||
final UniqueKey b = UniqueKey();
|
||||
final List<RouteBase> routes = <RouteBase>[
|
||||
ShellRoute(
|
||||
builder: (_, __, Widget child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell')),
|
||||
body: child,
|
||||
);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/a',
|
||||
builder: (_, __) => DummyScreen(key: a),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/b',
|
||||
builder: (_, __) => DummyScreen(key: b),
|
||||
)
|
||||
],
|
||||
),
|
||||
];
|
||||
final GoRouter router =
|
||||
await createRouter(routes, tester, initialLocation: '/a');
|
||||
|
||||
expect(find.text('shell'), findsOneWidget);
|
||||
expect(find.byKey(a), findsOneWidget);
|
||||
|
||||
router.replace('/b');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsOneWidget);
|
||||
expect(find.byKey(a), findsNothing);
|
||||
expect(find.byKey(b), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('push from outside of shell route', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/130406.
|
||||
final UniqueKey a = UniqueKey();
|
||||
final UniqueKey b = UniqueKey();
|
||||
final List<RouteBase> routes = <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/a',
|
||||
builder: (_, __) => DummyScreen(key: a),
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (_, __, Widget child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell')),
|
||||
body: child,
|
||||
);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/b',
|
||||
builder: (_, __) => DummyScreen(key: b),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
final GoRouter router =
|
||||
await createRouter(routes, tester, initialLocation: '/a');
|
||||
|
||||
expect(find.text('shell'), findsNothing);
|
||||
expect(find.byKey(a), findsOneWidget);
|
||||
|
||||
router.push('/b');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsOneWidget);
|
||||
expect(find.byKey(a), findsNothing);
|
||||
expect(find.byKey(b), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shell route reflect imperative push',
|
||||
(WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/125752.
|
||||
final UniqueKey home = UniqueKey();
|
||||
final UniqueKey a = UniqueKey();
|
||||
final List<RouteBase> routes = <RouteBase>[
|
||||
ShellRoute(
|
||||
builder: (_, GoRouterState state, Widget child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text('location: ${state.uri.path}')),
|
||||
body: child,
|
||||
);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (_, __) => DummyScreen(key: home),
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'a',
|
||||
builder: (_, __) => DummyScreen(key: a),
|
||||
),
|
||||
]),
|
||||
],
|
||||
),
|
||||
];
|
||||
final GoRouter router =
|
||||
await createRouter(routes, tester, initialLocation: '/a');
|
||||
|
||||
expect(find.text('location: /a'), findsOneWidget);
|
||||
expect(find.byKey(a), findsOneWidget);
|
||||
|
||||
router.pop();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('location: /'), findsOneWidget);
|
||||
expect(find.byKey(a), findsNothing);
|
||||
expect(find.byKey(home), findsOneWidget);
|
||||
|
||||
router.push('/a');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('location: /a'), findsOneWidget);
|
||||
expect(find.byKey(a), findsOneWidget);
|
||||
expect(find.byKey(home), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('push shell route in another shell route',
|
||||
(WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/120791.
|
||||
final UniqueKey b = UniqueKey();
|
||||
final UniqueKey a = UniqueKey();
|
||||
final List<RouteBase> routes = <RouteBase>[
|
||||
ShellRoute(
|
||||
builder: (_, __, Widget child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell1')),
|
||||
body: child,
|
||||
);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/a',
|
||||
builder: (_, __) => DummyScreen(key: a),
|
||||
),
|
||||
],
|
||||
),
|
||||
ShellRoute(
|
||||
builder: (_, __, Widget child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell2')),
|
||||
body: child,
|
||||
);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/b',
|
||||
builder: (_, __) => DummyScreen(key: b),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
final GoRouter router =
|
||||
await createRouter(routes, tester, initialLocation: '/a');
|
||||
|
||||
expect(find.text('shell1'), findsOneWidget);
|
||||
expect(find.byKey(a), findsOneWidget);
|
||||
|
||||
router.push('/b');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell1'), findsNothing);
|
||||
expect(find.byKey(a), findsNothing);
|
||||
expect(find.text('shell2'), findsOneWidget);
|
||||
expect(find.byKey(b), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('push inside or outside shell route',
|
||||
(WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/120665.
|
||||
final UniqueKey inside = UniqueKey();
|
||||
final UniqueKey outside = UniqueKey();
|
||||
final List<RouteBase> routes = <RouteBase>[
|
||||
ShellRoute(
|
||||
builder: (_, __, Widget child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell')),
|
||||
body: child,
|
||||
);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/in',
|
||||
builder: (_, __) => DummyScreen(key: inside),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/out',
|
||||
builder: (_, __) => DummyScreen(key: outside),
|
||||
),
|
||||
];
|
||||
final GoRouter router =
|
||||
await createRouter(routes, tester, initialLocation: '/out');
|
||||
|
||||
expect(find.text('shell'), findsNothing);
|
||||
expect(find.byKey(outside), findsOneWidget);
|
||||
|
||||
router.push('/in');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsOneWidget);
|
||||
expect(find.byKey(outside), findsNothing);
|
||||
expect(find.byKey(inside), findsOneWidget);
|
||||
|
||||
router.push('/out');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsNothing);
|
||||
expect(find.byKey(outside), findsOneWidget);
|
||||
expect(find.byKey(inside), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('complex case 1', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/113001.
|
||||
final UniqueKey a = UniqueKey();
|
||||
final UniqueKey b = UniqueKey();
|
||||
final UniqueKey c = UniqueKey();
|
||||
final UniqueKey d = UniqueKey();
|
||||
final UniqueKey e = UniqueKey();
|
||||
final List<RouteBase> routes = <RouteBase>[
|
||||
ShellRoute(
|
||||
builder: (_, __, Widget child) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('shell')),
|
||||
body: child,
|
||||
);
|
||||
},
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: '/a',
|
||||
builder: (_, __) => DummyScreen(key: a),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/c',
|
||||
builder: (_, __) => DummyScreen(key: c),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/d',
|
||||
builder: (_, __) => DummyScreen(key: d),
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'e',
|
||||
builder: (_, __) => DummyScreen(key: e),
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRoute(
|
||||
path: '/b',
|
||||
builder: (_, __) => DummyScreen(key: b),
|
||||
),
|
||||
];
|
||||
final GoRouter router =
|
||||
await createRouter(routes, tester, initialLocation: '/a');
|
||||
|
||||
expect(find.text('shell'), findsOneWidget);
|
||||
expect(find.byKey(a), findsOneWidget);
|
||||
|
||||
router.push('/b');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsNothing);
|
||||
expect(find.byKey(a), findsNothing);
|
||||
expect(find.byKey(b), findsOneWidget);
|
||||
|
||||
router.pop();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsOneWidget);
|
||||
expect(find.byKey(a), findsOneWidget);
|
||||
|
||||
router.go('/c');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsOneWidget);
|
||||
expect(find.byKey(c), findsOneWidget);
|
||||
|
||||
router.push('/d');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsNothing);
|
||||
expect(find.byKey(d), findsOneWidget);
|
||||
|
||||
router.push('/d/e');
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('shell'), findsNothing);
|
||||
expect(find.byKey(e), findsOneWidget);
|
||||
});
|
||||
}
|
@ -81,6 +81,33 @@ void main() {
|
||||
await tester.tap(find.text('My Page'));
|
||||
expect(router.latestPushedName, 'my_page');
|
||||
});
|
||||
|
||||
testWidgets('builder can access GoRouter', (WidgetTester tester) async {
|
||||
// Regression test for https://github.com/flutter/flutter/issues/110512.
|
||||
late final GoRouter buildContextRouter;
|
||||
final GoRouter router = GoRouter(
|
||||
initialLocation: '/',
|
||||
routes: <GoRoute>[
|
||||
GoRoute(
|
||||
path: '/',
|
||||
builder: (BuildContext context, __) {
|
||||
buildContextRouter = GoRouter.of(context);
|
||||
return const DummyScreen();
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp.router(
|
||||
routeInformationProvider: router.routeInformationProvider,
|
||||
routeInformationParser: router.routeInformationParser,
|
||||
routerDelegate: router.routerDelegate),
|
||||
);
|
||||
|
||||
expect(buildContextRouter, isNotNull);
|
||||
expect(buildContextRouter, equals(router));
|
||||
});
|
||||
}
|
||||
|
||||
bool setupInheritedGoRouterChange({
|
||||
|
@ -16,44 +16,20 @@ void main() {
|
||||
builder: _builder,
|
||||
);
|
||||
final Map<String, String> pathParameters = <String, String>{};
|
||||
final RouteMatch? match = RouteMatch.match(
|
||||
final List<RouteMatchBase> matches = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: '/users/123',
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
pathParameters: pathParameters,
|
||||
uri: Uri.parse('/users/123'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
);
|
||||
if (match == null) {
|
||||
fail('Null match');
|
||||
}
|
||||
expect(matches.length, 1);
|
||||
final RouteMatchBase match = matches.first;
|
||||
expect(match.route, route);
|
||||
expect(match.matchedLocation, '/users/123');
|
||||
expect(pathParameters['userId'], '123');
|
||||
expect(match.pageKey, isNotNull);
|
||||
});
|
||||
|
||||
test('matchedLocation', () {
|
||||
final GoRoute route = GoRoute(
|
||||
path: 'users/:userId',
|
||||
builder: _builder,
|
||||
);
|
||||
final Map<String, String> pathParameters = <String, String>{};
|
||||
final RouteMatch? match = RouteMatch.match(
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
if (match == null) {
|
||||
fail('Null match');
|
||||
}
|
||||
expect(match.route, route);
|
||||
expect(match.matchedLocation, '/home/users/123');
|
||||
expect(pathParameters['userId'], '123');
|
||||
expect(match.pageKey, isNotNull);
|
||||
});
|
||||
|
||||
test('ShellRoute has a unique pageKey', () {
|
||||
final ShellRoute route = ShellRoute(
|
||||
builder: _shellBuilder,
|
||||
@ -65,17 +41,14 @@ void main() {
|
||||
],
|
||||
);
|
||||
final Map<String, String> pathParameters = <String, String>{};
|
||||
final RouteMatch? match = RouteMatch.match(
|
||||
final List<RouteMatchBase> matches = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
uri: Uri.parse('/users/123'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
if (match == null) {
|
||||
fail('Null match');
|
||||
}
|
||||
expect(match.pageKey, isNotNull);
|
||||
expect(matches.length, 1);
|
||||
expect(matches.first.pageKey, isNotNull);
|
||||
});
|
||||
|
||||
test('ShellRoute Match has stable unique key', () {
|
||||
@ -89,51 +62,136 @@ void main() {
|
||||
],
|
||||
);
|
||||
final Map<String, String> pathParameters = <String, String>{};
|
||||
final RouteMatch? match1 = RouteMatch.match(
|
||||
final List<RouteMatchBase> matches1 = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
uri: Uri.parse('/users/123'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
);
|
||||
|
||||
final RouteMatch? match2 = RouteMatch.match(
|
||||
final List<RouteMatchBase> matches2 = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: 'users/1234',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
pathParameters: pathParameters,
|
||||
uri: Uri.parse('/users/1234'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
);
|
||||
|
||||
expect(match1!.pageKey, match2!.pageKey);
|
||||
expect(matches1.length, 1);
|
||||
expect(matches2.length, 1);
|
||||
expect(matches1.first.pageKey, matches2.first.pageKey);
|
||||
});
|
||||
|
||||
test('GoRoute Match has stable unique key', () {
|
||||
final GoRoute route = GoRoute(
|
||||
path: 'users/:userId',
|
||||
path: '/users/:userId',
|
||||
builder: _builder,
|
||||
);
|
||||
final Map<String, String> pathParameters = <String, String>{};
|
||||
final RouteMatch? match1 = RouteMatch.match(
|
||||
final List<RouteMatchBase> matches1 = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: 'users/123',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
uri: Uri.parse('/users/123'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
|
||||
final RouteMatch? match2 = RouteMatch.match(
|
||||
final List<RouteMatchBase> matches2 = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: 'users/1234',
|
||||
matchedLocation: '/home',
|
||||
matchedPath: '/home',
|
||||
uri: Uri.parse('/users/1234'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
pathParameters: pathParameters,
|
||||
);
|
||||
|
||||
expect(match1!.pageKey, match2!.pageKey);
|
||||
expect(matches1.length, 1);
|
||||
expect(matches2.length, 1);
|
||||
expect(matches1.first.pageKey, matches2.first.pageKey);
|
||||
});
|
||||
});
|
||||
|
||||
test('complex parentNavigatorKey works', () {
|
||||
final GlobalKey<NavigatorState> root = GlobalKey<NavigatorState>();
|
||||
final GlobalKey<NavigatorState> shell1 = GlobalKey<NavigatorState>();
|
||||
final GlobalKey<NavigatorState> shell2 = GlobalKey<NavigatorState>();
|
||||
final GoRoute route = GoRoute(
|
||||
path: '/',
|
||||
builder: _builder,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
navigatorKey: shell1,
|
||||
builder: _shellBuilder,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'a',
|
||||
builder: _builder,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
parentNavigatorKey: root,
|
||||
path: 'b',
|
||||
builder: _builder,
|
||||
routes: <RouteBase>[
|
||||
ShellRoute(
|
||||
navigatorKey: shell2,
|
||||
builder: _shellBuilder,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
path: 'c',
|
||||
builder: _builder,
|
||||
routes: <RouteBase>[
|
||||
GoRoute(
|
||||
parentNavigatorKey: root,
|
||||
path: 'd',
|
||||
builder: _builder,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
final Map<String, String> pathParameters = <String, String>{};
|
||||
final List<RouteMatchBase> matches = RouteMatchBase.match(
|
||||
route: route,
|
||||
pathParameters: pathParameters,
|
||||
uri: Uri.parse('/a/b/c/d'),
|
||||
rootNavigatorKey: root,
|
||||
);
|
||||
expect(matches.length, 4);
|
||||
expect(
|
||||
matches[0].route,
|
||||
isA<GoRoute>().having(
|
||||
(GoRoute route) => route.path,
|
||||
'path',
|
||||
'/',
|
||||
),
|
||||
);
|
||||
expect(
|
||||
matches[1].route,
|
||||
isA<ShellRoute>().having(
|
||||
(ShellRoute route) => route.navigatorKey,
|
||||
'navigator key',
|
||||
shell1,
|
||||
),
|
||||
);
|
||||
expect(
|
||||
matches[2].route,
|
||||
isA<GoRoute>().having(
|
||||
(GoRoute route) => route.path,
|
||||
'path',
|
||||
'b',
|
||||
),
|
||||
);
|
||||
expect(
|
||||
matches[3].route,
|
||||
isA<GoRoute>().having(
|
||||
(GoRoute route) => route.path,
|
||||
'path',
|
||||
'd',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
group('ImperativeRouteMatch', () {
|
||||
final RouteMatchList matchList1 = RouteMatchList(
|
||||
matches: <RouteMatch>[
|
||||
|
@ -35,37 +35,35 @@ void main() {
|
||||
const Placeholder(),
|
||||
);
|
||||
final Map<String, String> params1 = <String, String>{};
|
||||
final RouteMatch match1 = RouteMatch.match(
|
||||
final List<RouteMatchBase> match1 = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: '/page-0',
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
uri: Uri.parse('/page-0'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
pathParameters: params1,
|
||||
)!;
|
||||
);
|
||||
|
||||
final Map<String, String> params2 = <String, String>{};
|
||||
final RouteMatch match2 = RouteMatch.match(
|
||||
final List<RouteMatchBase> match2 = RouteMatchBase.match(
|
||||
route: route,
|
||||
remainingLocation: '/page-0',
|
||||
matchedLocation: '',
|
||||
matchedPath: '',
|
||||
uri: Uri.parse('/page-0'),
|
||||
rootNavigatorKey: GlobalKey<NavigatorState>(),
|
||||
pathParameters: params2,
|
||||
)!;
|
||||
);
|
||||
|
||||
final RouteMatchList matches1 = RouteMatchList(
|
||||
matches: <RouteMatch>[match1],
|
||||
matches: match1,
|
||||
uri: Uri.parse(''),
|
||||
pathParameters: params1,
|
||||
);
|
||||
|
||||
final RouteMatchList matches2 = RouteMatchList(
|
||||
matches: <RouteMatch>[match2],
|
||||
matches: match2,
|
||||
uri: Uri.parse(''),
|
||||
pathParameters: params2,
|
||||
);
|
||||
|
||||
final RouteMatchList matches3 = RouteMatchList(
|
||||
matches: <RouteMatch>[match2],
|
||||
matches: match2,
|
||||
uri: Uri.parse('/page-0'),
|
||||
pathParameters: params2,
|
||||
);
|
||||
|
@ -62,7 +62,7 @@ void main() {
|
||||
RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation('/'), context);
|
||||
List<RouteMatch> matches = matchesObj.matches;
|
||||
List<RouteMatchBase> matches = matchesObj.matches;
|
||||
expect(matches.length, 1);
|
||||
expect(matchesObj.uri.toString(), '/');
|
||||
expect(matchesObj.extra, isNull);
|
||||
@ -238,7 +238,7 @@ void main() {
|
||||
final RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation('/def'), context);
|
||||
final List<RouteMatch> matches = matchesObj.matches;
|
||||
final List<RouteMatchBase> matches = matchesObj.matches;
|
||||
expect(matches.length, 0);
|
||||
expect(matchesObj.uri.toString(), '/def');
|
||||
expect(matchesObj.extra, isNull);
|
||||
@ -303,7 +303,7 @@ void main() {
|
||||
final RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation('/123/family/456'), context);
|
||||
final List<RouteMatch> matches = matchesObj.matches;
|
||||
final List<RouteMatchBase> matches = matchesObj.matches;
|
||||
|
||||
expect(matches.length, 2);
|
||||
expect(matchesObj.uri.toString(), '/123/family/456');
|
||||
@ -347,7 +347,7 @@ void main() {
|
||||
final RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation('/random/uri'), context);
|
||||
final List<RouteMatch> matches = matchesObj.matches;
|
||||
final List<RouteMatchBase> matches = matchesObj.matches;
|
||||
|
||||
expect(matches.length, 2);
|
||||
expect(matchesObj.uri.toString(), '/123/family/345');
|
||||
@ -387,7 +387,7 @@ void main() {
|
||||
final RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation('/redirect'), context);
|
||||
final List<RouteMatch> matches = matchesObj.matches;
|
||||
final List<RouteMatchBase> matches = matchesObj.matches;
|
||||
|
||||
expect(matches.length, 2);
|
||||
expect(matchesObj.uri.toString(), '/123/family/345');
|
||||
@ -440,7 +440,7 @@ void main() {
|
||||
final RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation('/abd'), context);
|
||||
final List<RouteMatch> matches = matchesObj.matches;
|
||||
final List<RouteMatchBase> matches = matchesObj.matches;
|
||||
|
||||
expect(matches, hasLength(0));
|
||||
expect(matchesObj.error, isNotNull);
|
||||
@ -484,9 +484,11 @@ void main() {
|
||||
final RouteMatchList matchesObj =
|
||||
await parser.parseRouteInformationWithDependencies(
|
||||
createRouteInformation('/a'), context);
|
||||
final List<RouteMatch> matches = matchesObj.matches;
|
||||
final List<RouteMatchBase> matches = matchesObj.matches;
|
||||
|
||||
expect(matches, hasLength(2));
|
||||
expect(matches, hasLength(1));
|
||||
final ShellRouteMatch match = matches.first as ShellRouteMatch;
|
||||
expect(match.matches, hasLength(1));
|
||||
expect(matchesObj.error, isNull);
|
||||
});
|
||||
}
|
||||
|
@ -356,14 +356,6 @@ StatefulShellRouteBuilder mockStackedShellBuilder = (BuildContext context,
|
||||
return navigationShell;
|
||||
};
|
||||
|
||||
RouteMatch createRouteMatch(RouteBase route, String location) {
|
||||
return RouteMatch(
|
||||
route: route,
|
||||
matchedLocation: location,
|
||||
pageKey: ValueKey<String>(location),
|
||||
);
|
||||
}
|
||||
|
||||
/// A routing config that is never going to change.
|
||||
class ConstantRoutingConfig extends ValueListenable<RoutingConfig> {
|
||||
const ConstantRoutingConfig(this.value);
|
||||
|
Reference in New Issue
Block a user