From cc3ac4fdc1db3f2e635e9fe96c7f65a8c8bec43d Mon Sep 17 00:00:00 2001 From: Affan Date: Thu, 5 Dec 2024 08:17:33 +0530 Subject: [PATCH 1/8] Previously, users had to manually append fragments hashes to URLs, whichcould lead to potential bugs. This update introduces a dedicated fragment parameter, allowing for more seamless and reliable URL navigation. --- packages/go_router/CHANGELOG.md | 4 +++ .../go_router/lib/src/misc/extensions.dart | 12 +++---- packages/go_router/lib/src/router.dart | 7 +++- packages/go_router/lib/src/state.dart | 6 +++- packages/go_router/pubspec.yaml | 2 +- packages/go_router/test/go_route_test.dart | 32 +++++++++++++++++++ packages/go_router/test/test_helpers.dart | 3 ++ 7 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index e2f174dbef07..1654efafa71d 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,7 @@ +## 14.7.0 + +- Adds fragment support to GoRouter, enabling direct specification and automatic handling of fragments in routes. + ## 14.6.2 - Replaces deprecated collection method usage. diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index c137022b8020..fd1729d42c30 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -30,13 +30,13 @@ extension GoRouterHelper on BuildContext { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + String? fragment, }) => - GoRouter.of(this).goNamed( - name, - pathParameters: pathParameters, - queryParameters: queryParameters, - extra: extra, - ); + GoRouter.of(this).goNamed(name, + pathParameters: pathParameters, + queryParameters: queryParameters, + extra: extra, + fragment: fragment); /// Push a location onto the page stack. /// diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index 11f40f505cac..e366bafc2b5d 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -366,10 +366,15 @@ class GoRouter implements RouterConfig { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + String? fragment, }) => + + /// Construct location with optional fragment, using null-safe navigation go( namedLocation(name, - pathParameters: pathParameters, queryParameters: queryParameters), + pathParameters: pathParameters, + queryParameters: queryParameters) + + ((fragment?.isNotEmpty ?? false) ? '#$fragment' : ''), extra: extra, ); diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart index 0a1fb54ba79b..0d43e0e7d807 100644 --- a/packages/go_router/lib/src/state.dart +++ b/packages/go_router/lib/src/state.dart @@ -157,9 +157,13 @@ class GoRouterState { String name, { Map pathParameters = const {}, Map queryParameters = const {}, + String? fragment, }) { + // Generate base location using configuration, with optional path and query parameters + // Then conditionally append fragment if it exists and is not empty return _configuration.namedLocation(name, - pathParameters: pathParameters, queryParameters: queryParameters); + pathParameters: pathParameters, queryParameters: queryParameters) + + ((fragment?.isNotEmpty ?? false) ? '#$fragment' : ''); } @override diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml index a5e2e26b3cf6..a99c53a6b5db 100644 --- a/packages/go_router/pubspec.yaml +++ b/packages/go_router/pubspec.yaml @@ -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: 14.6.2 +version: 14.7.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 diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart index 0ff437c0c1c7..053856be35fe 100644 --- a/packages/go_router/test/go_route_test.dart +++ b/packages/go_router/test/go_route_test.dart @@ -249,6 +249,38 @@ void main() { expect(tester.takeException(), isAssertionError); }); + testWidgets('throw if redirect to itself.', (WidgetTester tester) async { + final GoRouter router = await createRouter( + [ + GoRoute( + path: '/', + builder: (_, __) => const Text('home'), + routes: [ + GoRoute( + path: 'route', + name: 'route', // Named route + redirect: (_, __) => '/route', + routes: [ + GoRoute( + path: '1', + builder: (_, __) => const Text('/route/1/2'), + ), + ], + ), + ], + ), + ], + tester, + ); + expect(find.text('home'), findsOneWidget); + + router.goNamed('route', + fragment: '2'); // Use the name instead of the path + await tester.pumpAndSettle(); + // Should redirect to /route/1 without error. + expect(tester.takeException(), isAssertionError); + }); + testWidgets('throw if sub route does not conform with parent navigator key', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index e6f69c507885..936e8b67d1d0 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -85,6 +85,7 @@ class GoRouterGoNamedSpy extends GoRouter { Map? pathParameters; Map? queryParameters; Object? extra; + String? fragment; @override void goNamed( @@ -92,11 +93,13 @@ class GoRouterGoNamedSpy extends GoRouter { Map pathParameters = const {}, Map queryParameters = const {}, Object? extra, + String? fragment, }) { this.name = name; this.pathParameters = pathParameters; this.queryParameters = queryParameters; this.extra = extra; + this.fragment = fragment; } } From 7a641b45e9839b84ea548ee6630dde8f7d63a0ce Mon Sep 17 00:00:00 2001 From: Affan Date: Thu, 5 Dec 2024 08:33:44 +0530 Subject: [PATCH 2/8] extensions.dart file formatted using dart format to remove the Linux repo_checks error. --- packages/go_router/lib/src/misc/extensions.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index fd1729d42c30..e42ac7ca166e 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -36,7 +36,7 @@ extension GoRouterHelper on BuildContext { pathParameters: pathParameters, queryParameters: queryParameters, extra: extra, - fragment: fragment); + fragment: fragment); /// Push a location onto the page stack. /// From 2a0cf739361eb56b02b170c3c715245284c59894 Mon Sep 17 00:00:00 2001 From: Affan Date: Tue, 10 Dec 2024 07:32:28 +0530 Subject: [PATCH 3/8] Added the the fragment handling to the namedLocation api final location = context.namedLocation( 'details', fragment: 'section3', ); context.go(location); --- packages/go_router/lib/src/misc/extensions.dart | 5 ++++- packages/go_router/lib/src/router.dart | 12 ++++++------ packages/go_router/test/go_route_test.dart | 8 ++++++-- packages/go_router/test/test_helpers.dart | 11 ++++++----- 4 files changed, 22 insertions(+), 14 deletions(-) diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart index e42ac7ca166e..3c390c945988 100644 --- a/packages/go_router/lib/src/misc/extensions.dart +++ b/packages/go_router/lib/src/misc/extensions.dart @@ -16,9 +16,12 @@ extension GoRouterHelper on BuildContext { String name, { Map pathParameters = const {}, Map queryParameters = const {}, + String? fragment, }) => GoRouter.of(this).namedLocation(name, - pathParameters: pathParameters, queryParameters: queryParameters); + pathParameters: pathParameters, + queryParameters: queryParameters, + fragment: fragment); /// Navigate to a location. void go(String location, {Object? extra}) => diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index e366bafc2b5d..e8f918284c61 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -331,16 +331,16 @@ class GoRouter implements RouterConfig { /// Get a location from route name and parameters. /// This is useful for redirecting to a named location. - String namedLocation( - String name, { - Map pathParameters = const {}, - Map queryParameters = const {}, - }) => + String namedLocation(String name, + {Map pathParameters = const {}, + Map queryParameters = const {}, + String? fragment}) => configuration.namedLocation( name, pathParameters: pathParameters, queryParameters: queryParameters, - ); + ) + + ((fragment?.isNotEmpty ?? false) ? '#$fragment' : ''); /// Navigate to a URI location w/ optional query parameters, e.g. /// `/family/f2/person/p1?color=blue` diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart index 053856be35fe..2856374f1bb7 100644 --- a/packages/go_router/test/go_route_test.dart +++ b/packages/go_router/test/go_route_test.dart @@ -274,8 +274,12 @@ void main() { ); expect(find.text('home'), findsOneWidget); - router.goNamed('route', - fragment: '2'); // Use the name instead of the path + // Test namedLocation with fragment + final String locationWithFragment = + router.namedLocation('route', fragment: '2'); + expect(locationWithFragment, '/route#2'); + // Navigate using goNamed with fragment + router.goNamed('route', fragment: '2'); await tester.pumpAndSettle(); // Should redirect to /route/1 without error. expect(tester.takeException(), isAssertionError); diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart index 936e8b67d1d0..cb1c2acdb966 100644 --- a/packages/go_router/test/test_helpers.dart +++ b/packages/go_router/test/test_helpers.dart @@ -45,16 +45,17 @@ class GoRouterNamedLocationSpy extends GoRouter { String? name; Map? pathParameters; Map? queryParameters; + String? fragment; @override - String namedLocation( - String name, { - Map pathParameters = const {}, - Map queryParameters = const {}, - }) { + String namedLocation(String name, + {Map pathParameters = const {}, + Map queryParameters = const {}, + String? fragment}) { this.name = name; this.pathParameters = pathParameters; this.queryParameters = queryParameters; + this.fragment = fragment; return ''; } } From ebb8cdc81017d6f5fa1e7269f6b48601b4c3dd04 Mon Sep 17 00:00:00 2001 From: Affan Date: Tue, 17 Dec 2024 10:29:56 +0530 Subject: [PATCH 4/8] Fix: Include fragment in URI generated by GoRouter This fix ensures that the fragment is properly included when generating the URI using namedLocation in router.dart. --- packages/go_router/lib/src/configuration.dart | 7 +++++-- packages/go_router/lib/src/router.dart | 10 +++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart index cc671066218d..95fd10029133 100644 --- a/packages/go_router/lib/src/configuration.dart +++ b/packages/go_router/lib/src/configuration.dart @@ -253,12 +253,14 @@ class RouteConfiguration { String name, { Map pathParameters = const {}, Map queryParameters = const {}, + String? fragment, }) { assert(() { log('getting location for name: ' '"$name"' '${pathParameters.isEmpty ? '' : ', pathParameters: $pathParameters'}' - '${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}'); + '${queryParameters.isEmpty ? '' : ', queryParameters: $queryParameters'}' + '${fragment != null ? ', fragment: $fragment' : ''}'); return true; }()); assert(_nameToPath.containsKey(name), 'unknown route name: $name'); @@ -285,7 +287,8 @@ class RouteConfiguration { final String location = patternToPath(path, encodedParams); return Uri( path: location, - queryParameters: queryParameters.isEmpty ? null : queryParameters) + queryParameters: queryParameters.isEmpty ? null : queryParameters, + fragment: fragment) .toString(); } diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart index e8f918284c61..1070c42b047e 100644 --- a/packages/go_router/lib/src/router.dart +++ b/packages/go_router/lib/src/router.dart @@ -339,8 +339,8 @@ class GoRouter implements RouterConfig { name, pathParameters: pathParameters, queryParameters: queryParameters, - ) + - ((fragment?.isNotEmpty ?? false) ? '#$fragment' : ''); + fragment: fragment, + ); /// Navigate to a URI location w/ optional query parameters, e.g. /// `/family/f2/person/p1?color=blue` @@ -372,9 +372,9 @@ class GoRouter implements RouterConfig { /// Construct location with optional fragment, using null-safe navigation go( namedLocation(name, - pathParameters: pathParameters, - queryParameters: queryParameters) + - ((fragment?.isNotEmpty ?? false) ? '#$fragment' : ''), + pathParameters: pathParameters, + queryParameters: queryParameters, + fragment: fragment), extra: extra, ); From 62b6027e58f9e9c08ddc0dd05f030c4bdd7fd2a6 Mon Sep 17 00:00:00 2001 From: Affan Date: Tue, 17 Dec 2024 11:11:43 +0530 Subject: [PATCH 5/8] updated changeLog --- packages/go_router/CHANGELOG.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index 95d0cd595f76..3f85759a1da3 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,7 +1,3 @@ -## NEXT - -* Updates minimum supported SDK version to Flutter 3.22/Dart 3.4. - ## 14.7.0 - Adds fragment support to GoRouter, enabling direct specification and automatic handling of fragments in routes. @@ -22,7 +18,6 @@ - Adds preload support to StatefulShellRoute, configurable via `preload` parameter on StatefulShellBranch. - ## 14.4.1 - Adds `missing_code_block_language_in_doc_comment` lint. @@ -41,7 +36,7 @@ ## 14.2.8 -- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView. +- Updated custom_stateful_shell_route example to better support swiping in TabView as well as demonstration of the use of PageView. ## 14.2.7 @@ -1145,3 +1140,4 @@ ## 0.1.0 - squatting on the package name (I'm not too proud to admit it) + From b948c714280542356a4c6814a27ee15386f10bee Mon Sep 17 00:00:00 2001 From: Affan Date: Wed, 18 Dec 2024 09:36:52 +0530 Subject: [PATCH 6/8] Fix: Implement fragment-based redirection in GoRouter test - Added logic to redirect to '/route/1' if the fragment is '1' in the '/route' route. - Updated test to verify correct redirection and ensure no infinite redirects occur. - Ensured proper navigation based on fragment values using router.goNamed(). - Added assertions to check for expected behavior and prevent exceptions during navigation. --- packages/go_router/test/go_route_test.dart | 48 ++++++++++++++-------- 1 file changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart index 2856374f1bb7..a67f3a451d4d 100644 --- a/packages/go_router/test/go_route_test.dart +++ b/packages/go_router/test/go_route_test.dart @@ -249,21 +249,28 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('throw if redirect to itself.', (WidgetTester tester) async { + testWidgets('redirects to a valid route based on fragment.', (WidgetTester tester) async { final GoRouter router = await createRouter( [ GoRoute( - path: '/', - builder: (_, __) => const Text('home'), + path: '/', + builder: (_, __) => const Text('home'), routes: [ GoRoute( - path: 'route', - name: 'route', // Named route - redirect: (_, __) => '/route', + path: 'route', + name: 'route', + redirect: (context, state) { + // Redirection logic based on the fragment in the URI + if (state.uri.fragment == '1') { + // If fragment is "1", redirect to "/route/1" + return '/route/1'; + } + return null; // No redirection for other cases + }, routes: [ GoRoute( - path: '1', - builder: (_, __) => const Text('/route/1/2'), + path: '1', + builder: (_, __) => const Text('/route/1'), // Renders "/route/1" text ), ], ), @@ -272,19 +279,24 @@ void main() { ], tester, ); + // Verify that the root route ("/") initially displays the "home" text expect(find.text('home'), findsOneWidget); - // Test namedLocation with fragment - final String locationWithFragment = - router.namedLocation('route', fragment: '2'); - expect(locationWithFragment, '/route#2'); - // Navigate using goNamed with fragment - router.goNamed('route', fragment: '2'); - await tester.pumpAndSettle(); - // Should redirect to /route/1 without error. - expect(tester.takeException(), isAssertionError); - }); + // Generate a location string for the named route "route" with fragment "2" + final String locationWithFragment = router.namedLocation('route', fragment: '2'); + expect(locationWithFragment, '/route#2'); // Expect the generated location to be "/route#2" + + // Navigate to the named route "route" with fragment "1" + router.goNamed('route', fragment: '1'); + await tester.pumpAndSettle(); + // Verify that navigating to "/route" with fragment "1" redirects to "/route/1" + expect(find.text('/route/1'), findsOneWidget); + + // Ensure no exceptions occurred during navigation + expect(tester.takeException(), isNull); + }); + testWidgets('throw if sub route does not conform with parent navigator key', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); From 0816efd6627c0aaa8dfd2f2095db1e630bf9083c Mon Sep 17 00:00:00 2001 From: Affan Date: Wed, 18 Dec 2024 09:43:32 +0530 Subject: [PATCH 7/8] dart format --- packages/go_router/test/go_route_test.dart | 26 +++++++++++++--------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart index a67f3a451d4d..f061660f8f16 100644 --- a/packages/go_router/test/go_route_test.dart +++ b/packages/go_router/test/go_route_test.dart @@ -249,16 +249,17 @@ void main() { expect(tester.takeException(), isAssertionError); }); - testWidgets('redirects to a valid route based on fragment.', (WidgetTester tester) async { + testWidgets('redirects to a valid route based on fragment.', + (WidgetTester tester) async { final GoRouter router = await createRouter( [ GoRoute( - path: '/', - builder: (_, __) => const Text('home'), + path: '/', + builder: (_, __) => const Text('home'), routes: [ GoRoute( - path: 'route', - name: 'route', + path: 'route', + name: 'route', redirect: (context, state) { // Redirection logic based on the fragment in the URI if (state.uri.fragment == '1') { @@ -269,8 +270,9 @@ void main() { }, routes: [ GoRoute( - path: '1', - builder: (_, __) => const Text('/route/1'), // Renders "/route/1" text + path: '1', + builder: (_, __) => + const Text('/route/1'), // Renders "/route/1" text ), ], ), @@ -283,12 +285,14 @@ void main() { expect(find.text('home'), findsOneWidget); // Generate a location string for the named route "route" with fragment "2" - final String locationWithFragment = router.namedLocation('route', fragment: '2'); - expect(locationWithFragment, '/route#2'); // Expect the generated location to be "/route#2" + final String locationWithFragment = + router.namedLocation('route', fragment: '2'); + expect(locationWithFragment, + '/route#2'); // Expect the generated location to be "/route#2" // Navigate to the named route "route" with fragment "1" router.goNamed('route', fragment: '1'); - await tester.pumpAndSettle(); + await tester.pumpAndSettle(); // Verify that navigating to "/route" with fragment "1" redirects to "/route/1" expect(find.text('/route/1'), findsOneWidget); @@ -296,7 +300,7 @@ void main() { // Ensure no exceptions occurred during navigation expect(tester.takeException(), isNull); }); - + testWidgets('throw if sub route does not conform with parent navigator key', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); From 0fee1335162a3e32906f569483322af463ea8bef Mon Sep 17 00:00:00 2001 From: Affan Date: Wed, 18 Dec 2024 09:58:58 +0530 Subject: [PATCH 8/8] added anotation --- packages/go_router/test/go_route_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/go_router/test/go_route_test.dart b/packages/go_router/test/go_route_test.dart index f061660f8f16..752a1a16580f 100644 --- a/packages/go_router/test/go_route_test.dart +++ b/packages/go_router/test/go_route_test.dart @@ -260,7 +260,7 @@ void main() { GoRoute( path: 'route', name: 'route', - redirect: (context, state) { + redirect: (BuildContext context, GoRouterState state) { // Redirection logic based on the fragment in the URI if (state.uri.fragment == '1') { // If fragment is "1", redirect to "/route/1"