Skip to content

Commit ac08525

Browse files
[go _route] fragment parameter added (#8232)
### Description of Change This PR addresses the need for fragments/hashes to be treated as first-party parameters within the `go_router` package, as highlighted in [issue #150155](flutter/flutter#150155). Previously, users had to manually append the fragment to the URL, which could lead to potential bugs. With this update, the `fragment` is now a dedicated parameter, allowing for a more seamless and bug-free integration. #### Before: ```dart final location = context.namedLocation('some_route'); // const nested records // Manually adding the fragment, hoping there aren't any weird bugs surrounding it context.replace('$location#https://a.url/that?i=mightuse'); ``` #### After: ```dart // Directly passing the fragment as a parameter context.goNamed("details", fragment: 'https://a.url/that?i=mightuse'); // or final location = GoRouterState.of(context).namedLocation( 'details', fragment: 'section3', ); context.go(location); // or final location = context.namedLocation( 'details', fragment: 'section3', ); context.go(location); ``` ### Issues Fixed This PR resolves [issue #150155](flutter/flutter#150155). --- ## Pre-launch Checklist - [✔️ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [✔️ ] I read the [Tree Hygiene] page, which explains my responsibilities. - [✔️ ] I read and followed the [relevant style guides] and ran the auto-formatter. (Unlike the flutter/flutter repo, the flutter/packages repo does use `dart format`.) - [x] I signed the [CLA]. - [✔️ ] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` - [✔️ ] I [linked to at least one issue that this PR fixes] in the description above. - [✔️ ] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or this PR is [exempt from version changes]. - [ ✔️] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or this PR is [exempt from CHANGELOG changes]. - [✔️ ] I updated/added relevant documentation (doc comments with `///`). - [ ✔️] I added new tests to check the change I am making, or this PR is [test-exempt]. - [ ✔️] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md [Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md [relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [linked to at least one issue that this PR fixes]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview [pub versioning philosophy]: https://dart.dev/tools/pub/versioning [exempt from version changes]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#version [following repository CHANGELOG style]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog-style [exempt from CHANGELOG changes]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog [test-exempt]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests
1 parent 06abd68 commit ac08525

File tree

8 files changed

+102
-24
lines changed

8 files changed

+102
-24
lines changed

packages/go_router/CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 14.7.0
2+
3+
- Adds fragment support to GoRouter, enabling direct specification and automatic handling of fragments in routes.
4+
15
## 14.6.4
26

37
- Rephrases readme.
@@ -7,6 +11,7 @@
711
- Updates minimum supported SDK version to Flutter 3.22/Dart 3.4.
812
- Updates readme.
913

14+
1015
## 14.6.2
1116

1217
- Replaces deprecated collection method usage.
@@ -23,7 +28,6 @@
2328

2429
- Adds preload support to StatefulShellRoute, configurable via `preload` parameter on StatefulShellBranch.
2530

26-
2731
## 14.4.1
2832

2933
- Adds `missing_code_block_language_in_doc_comment` lint.
@@ -42,7 +46,7 @@
4246

4347
## 14.2.8
4448

45-
- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView.
49+
- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView.
4650

4751
## 14.2.7
4852

@@ -1146,3 +1150,4 @@
11461150
## 0.1.0
11471151

11481152
- squatting on the package name (I'm not too proud to admit it)
1153+

packages/go_router/lib/src/configuration.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,14 @@ class RouteConfiguration {
253253
String name, {
254254
Map<String, String> pathParameters = const <String, String>{},
255255
Map<String, dynamic> queryParameters = const <String, dynamic>{},
256+
String? fragment,
256257
}) {
257258
assert(() {
258259
log('getting location for name: '
259260
'"$name"'
260261
'${pathParameters.isEmpty ? '' : ', pathParameters: $pathParameters'}'
261-
'${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}');
262+
'${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}'
263+
'${fragment != null ? ', fragment: $fragment' : ''}');
262264
return true;
263265
}());
264266
assert(_nameToPath.containsKey(name), 'unknown route name: $name');
@@ -285,7 +287,8 @@ class RouteConfiguration {
285287
final String location = patternToPath(path, encodedParams);
286288
return Uri(
287289
path: location,
288-
queryParameters: queryParameters.isEmpty ? null : queryParameters)
290+
queryParameters: queryParameters.isEmpty ? null : queryParameters,
291+
fragment: fragment)
289292
.toString();
290293
}
291294

packages/go_router/lib/src/misc/extensions.dart

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@ extension GoRouterHelper on BuildContext {
1616
String name, {
1717
Map<String, String> pathParameters = const <String, String>{},
1818
Map<String, dynamic> queryParameters = const <String, dynamic>{},
19+
String? fragment,
1920
}) =>
2021
GoRouter.of(this).namedLocation(name,
21-
pathParameters: pathParameters, queryParameters: queryParameters);
22+
pathParameters: pathParameters,
23+
queryParameters: queryParameters,
24+
fragment: fragment);
2225

2326
/// Navigate to a location.
2427
void go(String location, {Object? extra}) =>
@@ -30,13 +33,13 @@ extension GoRouterHelper on BuildContext {
3033
Map<String, String> pathParameters = const <String, String>{},
3134
Map<String, dynamic> queryParameters = const <String, dynamic>{},
3235
Object? extra,
36+
String? fragment,
3337
}) =>
34-
GoRouter.of(this).goNamed(
35-
name,
36-
pathParameters: pathParameters,
37-
queryParameters: queryParameters,
38-
extra: extra,
39-
);
38+
GoRouter.of(this).goNamed(name,
39+
pathParameters: pathParameters,
40+
queryParameters: queryParameters,
41+
extra: extra,
42+
fragment: fragment);
4043

4144
/// Push a location onto the page stack.
4245
///

packages/go_router/lib/src/router.dart

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -331,15 +331,15 @@ class GoRouter implements RouterConfig<RouteMatchList> {
331331

332332
/// Get a location from route name and parameters.
333333
/// This is useful for redirecting to a named location.
334-
String namedLocation(
335-
String name, {
336-
Map<String, String> pathParameters = const <String, String>{},
337-
Map<String, dynamic> queryParameters = const <String, dynamic>{},
338-
}) =>
334+
String namedLocation(String name,
335+
{Map<String, String> pathParameters = const <String, String>{},
336+
Map<String, dynamic> queryParameters = const <String, dynamic>{},
337+
String? fragment}) =>
339338
configuration.namedLocation(
340339
name,
341340
pathParameters: pathParameters,
342341
queryParameters: queryParameters,
342+
fragment: fragment,
343343
);
344344

345345
/// Navigate to a URI location w/ optional query parameters, e.g.
@@ -366,10 +366,15 @@ class GoRouter implements RouterConfig<RouteMatchList> {
366366
Map<String, String> pathParameters = const <String, String>{},
367367
Map<String, dynamic> queryParameters = const <String, dynamic>{},
368368
Object? extra,
369+
String? fragment,
369370
}) =>
371+
372+
/// Construct location with optional fragment, using null-safe navigation
370373
go(
371374
namedLocation(name,
372-
pathParameters: pathParameters, queryParameters: queryParameters),
375+
pathParameters: pathParameters,
376+
queryParameters: queryParameters,
377+
fragment: fragment),
373378
extra: extra,
374379
);
375380

packages/go_router/lib/src/state.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,14 @@ class GoRouterState {
157157
String name, {
158158
Map<String, String> pathParameters = const <String, String>{},
159159
Map<String, String> queryParameters = const <String, String>{},
160+
String? fragment,
160161
}) {
162+
// Generate base location using configuration, with optional path and query parameters
163+
// Then conditionally append fragment if it exists and is not empty
161164
return _configuration.namedLocation(name,
162-
pathParameters: pathParameters, queryParameters: queryParameters);
165+
pathParameters: pathParameters,
166+
queryParameters: queryParameters,
167+
fragment: fragment);
163168
}
164169

165170
@override

packages/go_router/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
name: go_router
22
description: A declarative router for Flutter based on Navigation 2 supporting
33
deep linking, data-driven routes and more
4-
version: 14.6.4
4+
version: 14.7.0
5+
56
repository: https://github.com/flutter/packages/tree/main/packages/go_router
67
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+go_router%22
78

packages/go_router/test/go_route_test.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,58 @@ void main() {
249249
expect(tester.takeException(), isAssertionError);
250250
});
251251

252+
testWidgets('redirects to a valid route based on fragment.',
253+
(WidgetTester tester) async {
254+
final GoRouter router = await createRouter(
255+
<RouteBase>[
256+
GoRoute(
257+
path: '/',
258+
builder: (_, __) => const Text('home'),
259+
routes: <RouteBase>[
260+
GoRoute(
261+
path: 'route',
262+
name: 'route',
263+
redirect: (BuildContext context, GoRouterState state) {
264+
// Redirection logic based on the fragment in the URI
265+
if (state.uri.fragment == '1') {
266+
// If fragment is "1", redirect to "/route/1"
267+
return '/route/1';
268+
}
269+
return null; // No redirection for other cases
270+
},
271+
routes: <RouteBase>[
272+
GoRoute(
273+
path: '1',
274+
builder: (_, __) =>
275+
const Text('/route/1'), // Renders "/route/1" text
276+
),
277+
],
278+
),
279+
],
280+
),
281+
],
282+
tester,
283+
);
284+
// Verify that the root route ("/") initially displays the "home" text
285+
expect(find.text('home'), findsOneWidget);
286+
287+
// Generate a location string for the named route "route" with fragment "2"
288+
final String locationWithFragment =
289+
router.namedLocation('route', fragment: '2');
290+
expect(locationWithFragment,
291+
'/route#2'); // Expect the generated location to be "/route#2"
292+
293+
// Navigate to the named route "route" with fragment "1"
294+
router.goNamed('route', fragment: '1');
295+
await tester.pumpAndSettle();
296+
297+
// Verify that navigating to "/route" with fragment "1" redirects to "/route/1"
298+
expect(find.text('/route/1'), findsOneWidget);
299+
300+
// Ensure no exceptions occurred during navigation
301+
expect(tester.takeException(), isNull);
302+
});
303+
252304
testWidgets('throw if sub route does not conform with parent navigator key',
253305
(WidgetTester tester) async {
254306
final GlobalKey<NavigatorState> key1 = GlobalKey<NavigatorState>();

packages/go_router/test/test_helpers.dart

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,17 @@ class GoRouterNamedLocationSpy extends GoRouter {
4545
String? name;
4646
Map<String, String>? pathParameters;
4747
Map<String, dynamic>? queryParameters;
48+
String? fragment;
4849

4950
@override
50-
String namedLocation(
51-
String name, {
52-
Map<String, String> pathParameters = const <String, String>{},
53-
Map<String, dynamic> queryParameters = const <String, dynamic>{},
54-
}) {
51+
String namedLocation(String name,
52+
{Map<String, String> pathParameters = const <String, String>{},
53+
Map<String, dynamic> queryParameters = const <String, dynamic>{},
54+
String? fragment}) {
5555
this.name = name;
5656
this.pathParameters = pathParameters;
5757
this.queryParameters = queryParameters;
58+
this.fragment = fragment;
5859
return '';
5960
}
6061
}
@@ -85,18 +86,21 @@ class GoRouterGoNamedSpy extends GoRouter {
8586
Map<String, String>? pathParameters;
8687
Map<String, dynamic>? queryParameters;
8788
Object? extra;
89+
String? fragment;
8890

8991
@override
9092
void goNamed(
9193
String name, {
9294
Map<String, String> pathParameters = const <String, String>{},
9395
Map<String, dynamic> queryParameters = const <String, dynamic>{},
9496
Object? extra,
97+
String? fragment,
9598
}) {
9699
this.name = name;
97100
this.pathParameters = pathParameters;
98101
this.queryParameters = queryParameters;
99102
this.extra = extra;
103+
this.fragment = fragment;
100104
}
101105
}
102106

0 commit comments

Comments
 (0)