Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
02689ff
- Adds `GoRouter.goRelative`
ThangVuNguyenViet May 29, 2024
e8ac4ee
add go_relative example for go_router
ThangVuNguyenViet May 29, 2024
c42151c
add go_relative_test for the example
ThangVuNguyenViet May 29, 2024
022db98
fix failed test
ThangVuNguyenViet May 29, 2024
812498e
replace goRelative with go('./$path')
ThangVuNguyenViet Jun 19, 2024
07b63ad
Commit missing files during merge
ThangVuNguyenViet Jun 19, 2024
c025d86
update changelog & version
ThangVuNguyenViet Jun 19, 2024
3fb6463
Prevent concatenateUris from adding trailing redundant '?'. Add test …
ThangVuNguyenViet Jun 19, 2024
205d096
Add more `concatenateUris` test. Fix joining params
ThangVuNguyenViet Jun 19, 2024
8e4db8e
update example description
ThangVuNguyenViet Jul 10, 2024
0ead0af
Make concatenateUris not merging parameters, only take them from chil…
ThangVuNguyenViet Jul 19, 2024
9407200
Refactor example & its test
ThangVuNguyenViet Jul 19, 2024
6830539
Remove TypedRelativeGoRoute
ThangVuNguyenViet Sep 6, 2024
43eb497
Add missing import
ThangVuNguyenViet Sep 6, 2024
2dd80d9
add fragment test to concatenateUris
ThangVuNguyenViet Sep 24, 2024
3071a53
Merge branch 'main' into go_router/go-relative
ThangVuNguyenViet Sep 24, 2024
4dba574
Merge branch 'main' of github.com:flutter/packages into go_router/go-…
ThangVuNguyenViet Nov 3, 2024
fb61d97
Merge branch 'main' into go_router/go-relative
ThangVuNguyenViet Nov 7, 2024
d1ebfeb
bump version to 14.4.2 in pubspec.yaml
ThangVuNguyenViet Nov 8, 2024
c246bc3
Merge branch 'main' into go_router/go-relative
chunhtai Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
- Adds GoRouter.goRelative
- Adds `TypedRelativeGoRoute`
  • Loading branch information
ThangVuNguyenViet committed Sep 6, 2024
commit 02689ff17952438d6c4fdc9275cce87d37bc7fe5
27 changes: 27 additions & 0 deletions packages/go_router/lib/src/information_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,33 @@ class GoRouteInformationProvider extends RouteInformationProvider
);
}

/// Relatively go to [relativeLocation].
void goRelative(String relativeLocation, {Object? extra}) {
assert(
!relativeLocation.startsWith('/'),
"Relative locations must not start with a '/'.",
);

final Uri currentUri = value.uri;
Uri newUri = Uri.parse(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just modify the path and use currentUri.replace(path: newPath)
This way you don't need to worry about other thing such as host/scheme/fragment/queryparameters

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gotcha. I move this in path_utils as concatenateUris

currentUri.path.endsWith('/')
? '${currentUri.path}$relativeLocation'
: '${currentUri.path}/$relativeLocation',
);
newUri = newUri.replace(queryParameters: <String, dynamic>{
...currentUri.queryParameters,
...newUri.queryParameters,
});

_setValue(
newUri.toString(),
RouteInformationState<void>(
extra: extra,
type: NavigatingType.go,
),
);
}

/// Restores the current route matches with the `matchList`.
void restore(String location, {required RouteMatchList matchList}) {
_setValue(
Expand Down
7 changes: 7 additions & 0 deletions packages/go_router/lib/src/misc/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ extension GoRouterHelper on BuildContext {
void go(String location, {Object? extra}) =>
GoRouter.of(this).go(location, extra: extra);

/// Navigate relative to a location.
void goRelative(String location, {Object? extra}) =>
GoRouter.of(this).goRelative(
location,
extra: extra,
);

/// Navigate to a named route.
void goNamed(
String name, {
Expand Down
22 changes: 22 additions & 0 deletions packages/go_router/lib/src/route_data.dart
Original file line number Diff line number Diff line change
Expand Up @@ -390,6 +390,28 @@ class TypedGoRoute<T extends GoRouteData> extends TypedRoute<T> {
final List<TypedRoute<RouteData>> routes;
}

/// A superclass for each typed go route descendant
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you also create a go_router_builder pr that shows how this will be used?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gotcha

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's #7174 @chunhtai , and I also updated the PR desc to reflect that

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chunhtai I've removed this class as discussed in the #7174

class TypedRelativeGoRoute<T extends GoRouteData> extends TypedRoute<T> {
/// Default const constructor
const TypedRelativeGoRoute({
required this.path,
this.routes = const <TypedRoute<RouteData>>[],
});

/// The relative path that corresponds to this route.
///
/// See [GoRoute.path].
///
///
final String path;

/// Child route definitions.
///
/// See [RouteBase.routes].
final List<TypedRoute<RouteData>> routes;
}

/// A superclass for each typed shell route descendant
@Target(<TargetKind>{TargetKind.library, TargetKind.classType})
class TypedShellRoute<T extends ShellRouteData> extends TypedRoute<T> {
Expand Down
9 changes: 9 additions & 0 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,15 @@ class GoRouter implements RouterConfig<RouteMatchList> {
routeInformationProvider.go(location, extra: extra);
}

/// Navigate to a URI location by appending [relativeLocation] to the current [GoRouterState.matchedLocation] w/ optional query parameters, e.g.
void goRelative(
String relativeLocation, {
Object? extra,
}) {
log('going relative to $relativeLocation');
routeInformationProvider.goRelative(relativeLocation, extra: extra);
}

/// Restore the RouteMatchList
void restore(RouteMatchList matchList) {
log('restoring ${matchList.uri}');
Expand Down
277 changes: 277 additions & 0 deletions packages/go_router/test/go_router_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1874,6 +1874,283 @@ void main() {
});
});

group('go relative', () {
testWidgets('from default route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
],
),
];

final GoRouter router = await createRouter(routes, tester);
router.goRelative('login');
await tester.pumpAndSettle();
expect(find.byType(LoginScreen), findsOneWidget);
});

testWidgets('from non-default route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'login',
builder: (BuildContext context, GoRouterState state) =>
const LoginScreen(),
),
],
),
];

final GoRouter router = await createRouter(routes, tester);
router.go('/home');
router.goRelative('login');
await tester.pumpAndSettle();
expect(find.byType(LoginScreen), findsOneWidget);
});

testWidgets('match w/ path params', (WidgetTester tester) async {
const String fid = 'f2';
const String pid = 'p1';

final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family/:fid',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
name: 'person',
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) {
expect(state.pathParameters,
<String, String>{'fid': fid, 'pid': pid});
return const PersonScreen('dummy', 'dummy');
},
),
],
),
],
),
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');
router.go('/');

router.goRelative('family/$fid');
await tester.pumpAndSettle();
expect(find.byType(FamilyScreen), findsOneWidget);

router.goRelative('person/$pid');
await tester.pumpAndSettle();
expect(find.byType(PersonScreen), findsOneWidget);
});

testWidgets('match w/ query params', (WidgetTester tester) async {
const String fid = 'f2';
const String pid = 'p1';

final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
path: 'person',
builder: (BuildContext context, GoRouterState state) {
expect(state.uri.queryParameters,
<String, String>{'fid': fid, 'pid': pid});
return const PersonScreen('dummy', 'dummy');
},
),
],
),
],
),
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');

router.goRelative('family?fid=$fid');
await tester.pumpAndSettle();
expect(find.byType(FamilyScreen), findsOneWidget);

router.goRelative('person?pid=$pid');
await tester.pumpAndSettle();
expect(find.byType(PersonScreen), findsOneWidget);
});

testWidgets('too few params', (WidgetTester tester) async {
const String pid = 'p1';

final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family/:fid',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
path: 'person/:pid',
builder: (BuildContext context, GoRouterState state) =>
const PersonScreen('dummy', 'dummy'),
),
],
),
],
),
];
// await expectLater(() async {
final GoRouter router = await createRouter(
routes,
tester,
initialLocation: '/home',
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
router.goRelative('family/person/$pid');
await tester.pumpAndSettle();
expect(find.byType(TestErrorScreen), findsOneWidget);

final List<RouteMatchBase> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
});

testWidgets('match no route', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: (BuildContext context, GoRouterState state) =>
const HomeScreen(),
routes: <GoRoute>[
GoRoute(
path: 'family',
builder: (BuildContext context, GoRouterState state) =>
const FamilyScreen('dummy'),
routes: <GoRoute>[
GoRoute(
path: 'person',
builder: (BuildContext context, GoRouterState state) =>
const PersonScreen('dummy', 'dummy'),
),
],
),
],
),
];

final GoRouter router = await createRouter(
routes,
tester,
initialLocation: '/home',
errorBuilder: (BuildContext context, GoRouterState state) =>
TestErrorScreen(state.error!),
);
router.go('person');

await tester.pumpAndSettle();
expect(find.byType(TestErrorScreen), findsOneWidget);

final List<RouteMatchBase> matches =
router.routerDelegate.currentConfiguration.matches;
expect(matches, hasLength(0));
});

testWidgets('preserve path param spaces and slashes',
(WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: dummy,
routes: <RouteBase>[
GoRoute(
path: 'page1/:param1',
builder: (BuildContext c, GoRouterState s) {
expect(s.pathParameters['param1'], param1);
return const DummyScreen();
},
),
],
)
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');
final String loc = 'page1/${Uri.encodeComponent(param1)}';
router.goRelative(loc);

await tester.pumpAndSettle();
expect(find.byType(DummyScreen), findsOneWidget);

final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.pathParameters['param1'], param1);
});

testWidgets('preserve query param spaces and slashes',
(WidgetTester tester) async {
const String param1 = 'param w/ spaces and slashes';
final List<GoRoute> routes = <GoRoute>[
GoRoute(
path: '/home',
builder: dummy,
routes: <RouteBase>[
GoRoute(
path: 'page1',
builder: (BuildContext c, GoRouterState s) {
expect(s.uri.queryParameters['param1'], param1);
return const DummyScreen();
},
),
],
)
];

final GoRouter router =
await createRouter(routes, tester, initialLocation: '/home');

router.goRelative(Uri(
path: 'page1',
queryParameters: <String, dynamic>{'param1': param1},
).toString());

await tester.pumpAndSettle();
expect(find.byType(DummyScreen), findsOneWidget);

final RouteMatchList matches = router.routerDelegate.currentConfiguration;
expect(matches.uri.queryParameters['param1'], param1);
});
});

group('redirects', () {
testWidgets('top-level redirect', (WidgetTester tester) async {
final List<GoRoute> routes = <GoRoute>[
Expand Down