[go_router] Make `queryParams` a `Map<String, dynamic>` (#2392)
diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index b97be1b..7ba2b9c 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 4.3.0
+
+- Allows `Map<String, dynamic>` maps as `queryParams` of `goNamed`, `replacedName`, `pushNamed` and `namedLocation`.
+
## 4.2.9
* Updates text theme parameters to avoid deprecation issues.
diff --git a/packages/go_router/lib/src/builder.dart b/packages/go_router/lib/src/builder.dart
index 4e27220..7df6747 100644
--- a/packages/go_router/lib/src/builder.dart
+++ b/packages/go_router/lib/src/builder.dart
@@ -93,6 +93,7 @@
subloc: uri.path,
name: null,
queryParams: uri.queryParameters,
+ queryParametersAll: uri.queryParametersAll,
error: error,
),
),
@@ -121,6 +122,7 @@
subloc: uri.path,
// pass along the query params 'cuz that's all we have right now
queryParams: uri.queryParameters,
+ queryParametersAll: uri.queryParametersAll,
// pass along the error, if there is one
error: error,
),
@@ -186,6 +188,7 @@
params: params,
error: match.error,
queryParams: match.queryParams,
+ queryParametersAll: match.queryParametersAll,
extra: match.extra,
pageKey: match.pageKey, // push() remaps the page key for uniqueness
);
diff --git a/packages/go_router/lib/src/configuration.dart b/packages/go_router/lib/src/configuration.dart
index c11c230..47262db 100644
--- a/packages/go_router/lib/src/configuration.dart
+++ b/packages/go_router/lib/src/configuration.dart
@@ -48,7 +48,7 @@
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
}) {
assert(() {
log.info('getting location for name: '
diff --git a/packages/go_router/lib/src/delegate.dart b/packages/go_router/lib/src/delegate.dart
index 29f0f25..de0f81a 100644
--- a/packages/go_router/lib/src/delegate.dart
+++ b/packages/go_router/lib/src/delegate.dart
@@ -60,6 +60,7 @@
fullpath: match.fullpath,
encodedParams: match.encodedParams,
queryParams: match.queryParams,
+ queryParametersAll: match.queryParametersAll,
extra: match.extra,
error: match.error,
pageKey: pageKey,
diff --git a/packages/go_router/lib/src/match.dart b/packages/go_router/lib/src/match.dart
index d7f096e..d65533c 100644
--- a/packages/go_router/lib/src/match.dart
+++ b/packages/go_router/lib/src/match.dart
@@ -18,10 +18,11 @@
required this.fullpath,
required this.encodedParams,
required this.queryParams,
+ required this.queryParametersAll,
required this.extra,
required this.error,
this.pageKey,
- }) : fullUriString = _addQueryParams(subloc, queryParams),
+ }) : fullUriString = _addQueryParams(subloc, queryParametersAll),
assert(subloc.startsWith('/')),
assert(Uri.parse(subloc).queryParameters.isEmpty),
assert(fullpath.startsWith('/')),
@@ -41,6 +42,7 @@
required String parentSubloc, // e.g. /family/f2
required String fullpath, // e.g. /family/:fid/person/:pid
required Map<String, String> queryParams,
+ required Map<String, List<String>> queryParametersAll,
required Object? extra,
}) {
assert(!route.path.contains('//'));
@@ -59,6 +61,7 @@
fullpath: fullpath,
encodedParams: encodedParams,
queryParams: queryParams,
+ queryParametersAll: queryParametersAll,
extra: extra,
error: null,
);
@@ -76,9 +79,35 @@
/// Parameters for the matched route, URI-encoded.
final Map<String, String> encodedParams;
- /// Query parameters for the matched route.
+ /// The URI query split into a map according to the rules specified for FORM
+ /// post in the [HTML 4.01 specification section
+ /// 17.13.4](https://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4
+ /// "HTML 4.01 section 17.13.4").
+ ///
+ /// If a key occurs more than once in the query string, it is mapped to an
+ /// arbitrary choice of possible value.
+ ///
+ /// If the request is `a/b/?q1=v1&q2=v2&q2=v3`, then [queryParameter] will be
+ /// `{q1: 'v1', q2: 'v2'}`.
+ ///
+ /// See also
+ /// * [queryParametersAll] that can provide a map that maps keys to all of
+ /// their values.
final Map<String, String> queryParams;
+ /// Returns the URI query split into a map according to the rules specified
+ /// for FORM post in the [HTML 4.01 specification section
+ /// 17.13.4](https://www.w3.org/TR/REC-html40/interact/forms.html#h-17.13.4
+ /// "HTML 4.01 section 17.13.4").
+ ///
+ /// Keys are mapped to lists of their values. If a key occurs only once, its
+ /// value is a singleton list. If a key occurs with no value, the empty string
+ /// is used as the value for that occurrence.
+ ///
+ /// If the request is `a/b/?q1=v1&q2=v2&q2=v3`, then [queryParameterAll] with
+ /// be `{q1: ['v1'], q2: ['v2', 'v3']}`.
+ final Map<String, List<String>> queryParametersAll;
+
/// An extra object to pass along with the navigation.
final Object? extra;
@@ -91,12 +120,14 @@
/// The full uri string
final String fullUriString; // e.g. /family/12?query=14
- static String _addQueryParams(String loc, Map<String, String> queryParams) {
+ static String _addQueryParams(
+ String loc, Map<String, dynamic> queryParametersAll) {
final Uri uri = Uri.parse(loc);
assert(uri.queryParameters.isEmpty);
return Uri(
path: uri.path,
- queryParameters: queryParams.isEmpty ? null : queryParams)
+ queryParameters:
+ queryParametersAll.isEmpty ? null : queryParametersAll)
.toString();
}
diff --git a/packages/go_router/lib/src/matching.dart b/packages/go_router/lib/src/matching.dart
index 6135cff..b7681c6 100644
--- a/packages/go_router/lib/src/matching.dart
+++ b/packages/go_router/lib/src/matching.dart
@@ -31,6 +31,7 @@
parentFullpath: '',
parentSubloc: '',
queryParams: uri.queryParameters,
+ queryParametersAll: uri.queryParametersAll,
extra: extra,
);
@@ -121,6 +122,7 @@
required List<GoRoute> routes,
required String parentFullpath,
required Map<String, String> queryParams,
+ required Map<String, List<String>> queryParametersAll,
required Object? extra,
}) {
bool debugGatherAllMatches = false;
@@ -138,6 +140,7 @@
parentSubloc: parentSubloc,
fullpath: fullpath,
queryParams: queryParams,
+ queryParametersAll: queryParametersAll,
extra: extra,
);
@@ -166,6 +169,7 @@
routes: route.routes,
parentFullpath: fullpath,
queryParams: queryParams,
+ queryParametersAll: queryParametersAll,
extra: extra,
).toList();
diff --git a/packages/go_router/lib/src/misc/extensions.dart b/packages/go_router/lib/src/misc/extensions.dart
index c818c2c..65e45cb 100644
--- a/packages/go_router/lib/src/misc/extensions.dart
+++ b/packages/go_router/lib/src/misc/extensions.dart
@@ -13,7 +13,7 @@
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
}) =>
GoRouter.of(this)
.namedLocation(name, params: params, queryParams: queryParams);
@@ -26,7 +26,7 @@
void goNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) =>
GoRouter.of(this).goNamed(
@@ -44,7 +44,7 @@
void pushNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) =>
GoRouter.of(this).pushNamed(
@@ -80,7 +80,7 @@
void replaceNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) =>
GoRouter.of(this).replaceNamed(
diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart
index 3f716e4..9959553 100644
--- a/packages/go_router/lib/src/parser.dart
+++ b/packages/go_router/lib/src/parser.dart
@@ -113,6 +113,7 @@
fullpath: uri.path,
encodedParams: <String, String>{},
queryParams: uri.queryParameters,
+ queryParametersAll: uri.queryParametersAll,
extra: null,
error: error,
route: GoRoute(
diff --git a/packages/go_router/lib/src/redirection.dart b/packages/go_router/lib/src/redirection.dart
index 0abeb52..e494ee6 100644
--- a/packages/go_router/lib/src/redirection.dart
+++ b/packages/go_router/lib/src/redirection.dart
@@ -39,6 +39,7 @@
// sub-location to match route.redirect
subloc: uri.path,
queryParams: uri.queryParameters,
+ queryParametersAll: uri.queryParametersAll,
extra: extra,
),
);
@@ -81,6 +82,7 @@
extra: top.extra,
params: top.decodedParams,
queryParams: top.queryParams,
+ queryParametersAll: top.queryParametersAll,
),
);
diff --git a/packages/go_router/lib/src/router.dart b/packages/go_router/lib/src/router.dart
index 644480f..95a26a5 100644
--- a/packages/go_router/lib/src/router.dart
+++ b/packages/go_router/lib/src/router.dart
@@ -142,7 +142,7 @@
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
}) =>
_routeInformationParser.configuration.namedLocation(
name,
@@ -167,7 +167,7 @@
void goNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) =>
go(
@@ -195,7 +195,7 @@
void pushNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) =>
push(
@@ -229,7 +229,7 @@
void replaceNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) {
replace(
diff --git a/packages/go_router/lib/src/state.dart b/packages/go_router/lib/src/state.dart
index 5e9c4ab..34b6449 100644
--- a/packages/go_router/lib/src/state.dart
+++ b/packages/go_router/lib/src/state.dart
@@ -20,6 +20,7 @@
this.fullpath,
this.params = const <String, String>{},
this.queryParams = const <String, String>{},
+ this.queryParametersAll = const <String, List<String>>{},
this.extra,
this.error,
ValueKey<String>? pageKey,
@@ -56,6 +57,10 @@
/// The query parameters for the location, e.g. {'from': '/family/f2'}
final Map<String, String> queryParams;
+ /// The query parameters for the location,
+ /// e.g. `{'q1': ['v1'], 'q2': ['v2', 'v3']}`
+ final Map<String, List<String>> queryParametersAll;
+
/// An extra object to pass along with the navigation.
final Object? extra;
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 2a54b88..5a72446 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: 4.2.9
+version: 4.3.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_router_test.dart b/packages/go_router/test/go_router_test.dart
index e1b4000..2034688 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -1585,6 +1585,107 @@
expect(matches.last.decodedParams['fid'], fid);
expect(matches.last.decodedParams['pid'], pid);
});
+
+ testWidgets('goNames should allow dynamics values for queryParams',
+ (WidgetTester tester) async {
+ const Map<String, dynamic> queryParametersAll = <String, List<dynamic>>{
+ 'q1': <String>['v1'],
+ 'q2': <String>['v2', 'v3'],
+ };
+ void expectLocationWithQueryParams(String location) {
+ final Uri uri = Uri.parse(location);
+ expect(uri.path, '/page');
+ expect(uri.queryParametersAll, queryParametersAll);
+ }
+
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (BuildContext context, GoRouterState state) =>
+ const HomeScreen(),
+ ),
+ GoRoute(
+ name: 'page',
+ path: '/page',
+ builder: (BuildContext context, GoRouterState state) {
+ expect(state.queryParametersAll, queryParametersAll);
+ expectLocationWithQueryParams(state.location);
+ return DummyScreen(
+ queryParametersAll: state.queryParametersAll,
+ );
+ },
+ ),
+ ];
+
+ final GoRouter router = await createRouter(routes, tester);
+
+ router.goNamed('page', queryParams: const <String, dynamic>{
+ 'q1': 'v1',
+ 'q2': <String>['v2', 'v3'],
+ });
+ await tester.pump();
+ final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+
+ expect(matches, hasLength(1));
+ expectLocationWithQueryParams(router.location);
+ expect(
+ router.screenFor(matches.last),
+ isA<DummyScreen>().having(
+ (DummyScreen screen) => screen.queryParametersAll,
+ 'screen.queryParametersAll',
+ queryParametersAll,
+ ),
+ );
+ });
+ });
+
+ testWidgets('go should preserve the query parameters when navigating',
+ (WidgetTester tester) async {
+ const Map<String, dynamic> queryParametersAll = <String, List<dynamic>>{
+ 'q1': <String>['v1'],
+ 'q2': <String>['v2', 'v3'],
+ };
+ void expectLocationWithQueryParams(String location) {
+ final Uri uri = Uri.parse(location);
+ expect(uri.path, '/page');
+ expect(uri.queryParametersAll, queryParametersAll);
+ }
+
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (BuildContext context, GoRouterState state) =>
+ const HomeScreen(),
+ ),
+ GoRoute(
+ name: 'page',
+ path: '/page',
+ builder: (BuildContext context, GoRouterState state) {
+ expect(state.queryParametersAll, queryParametersAll);
+ expectLocationWithQueryParams(state.location);
+ return DummyScreen(
+ queryParametersAll: state.queryParametersAll,
+ );
+ },
+ ),
+ ];
+
+ final GoRouter router = await createRouter(routes, tester);
+
+ router.go('/page?q1=v1&q2=v2&q2=v3');
+ await tester.pump();
+ final List<RouteMatch> matches = router.routerDelegate.matches.matches;
+
+ expect(matches, hasLength(1));
+ expectLocationWithQueryParams(router.location);
+ expect(
+ router.screenFor(matches.last),
+ isA<DummyScreen>().having(
+ (DummyScreen screen) => screen.queryParametersAll,
+ 'screen.queryParametersAll',
+ queryParametersAll,
+ ),
+ );
});
group('refresh listenable', () {
diff --git a/packages/go_router/test/inherited_test.dart b/packages/go_router/test/inherited_test.dart
index 2592eed..920a61c 100644
--- a/packages/go_router/test/inherited_test.dart
+++ b/packages/go_router/test/inherited_test.dart
@@ -131,7 +131,7 @@
@override
void pushNamed(String name,
{Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra}) {
latestPushedName = name;
}
diff --git a/packages/go_router/test/parser_test.dart b/packages/go_router/test/parser_test.dart
index b943f12..bed6c27 100644
--- a/packages/go_router/test/parser_test.dart
+++ b/packages/go_router/test/parser_test.dart
@@ -111,6 +111,39 @@
'/abc?q=1&g=2');
});
+ test(
+ 'GoRouteInformationParser can retrieve route by name with query parameters',
+ () async {
+ final List<GoRoute> routes = <GoRoute>[
+ GoRoute(
+ path: '/',
+ builder: (_, __) => const Placeholder(),
+ routes: <GoRoute>[
+ GoRoute(
+ path: 'abc',
+ name: 'routeName',
+ builder: (_, __) => const Placeholder(),
+ ),
+ ],
+ ),
+ ];
+
+ final RouteConfiguration configuration = RouteConfiguration(
+ routes: routes,
+ redirectLimit: 100,
+ topRedirect: (_) => null,
+ );
+
+ expect(
+ configuration
+ .namedLocation('routeName', queryParams: const <String, dynamic>{
+ 'q1': 'v1',
+ 'q2': <String>['v2', 'v3'],
+ }),
+ '/abc?q1=v1&q2=v2&q2=v3',
+ );
+ });
+
test('GoRouteInformationParser returns error when unknown route', () async {
final List<GoRoute> routes = <GoRoute>[
GoRoute(
diff --git a/packages/go_router/test/test_helpers.dart b/packages/go_router/test/test_helpers.dart
index f0592a6..0078f55 100644
--- a/packages/go_router/test/test_helpers.dart
+++ b/packages/go_router/test/test_helpers.dart
@@ -45,13 +45,13 @@
String? name;
Map<String, String>? params;
- Map<String, String>? queryParams;
+ Map<String, dynamic>? queryParams;
@override
String namedLocation(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
}) {
this.name = name;
this.params = params;
@@ -78,14 +78,14 @@
String? name;
Map<String, String>? params;
- Map<String, String>? queryParams;
+ Map<String, dynamic>? queryParams;
Object? extra;
@override
void goNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) {
this.name = name;
@@ -113,14 +113,14 @@
String? name;
Map<String, String>? params;
- Map<String, String>? queryParams;
+ Map<String, dynamic>? queryParams;
Object? extra;
@override
void pushNamed(
String name, {
Map<String, String> params = const <String, String>{},
- Map<String, String> queryParams = const <String, String>{},
+ Map<String, dynamic> queryParams = const <String, dynamic>{},
Object? extra,
}) {
this.name = name;
@@ -218,7 +218,12 @@
}
class DummyScreen extends StatelessWidget {
- const DummyScreen({super.key});
+ const DummyScreen({
+ this.queryParametersAll = const <String, dynamic>{},
+ super.key,
+ });
+
+ final Map<String, dynamic> queryParametersAll;
@override
Widget build(BuildContext context) => const Placeholder();