[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();