[go_router] If there is more than one route to match, use the first match. (#1995)

diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md
index c8814d4..a37fabe 100644
--- a/packages/go_router/CHANGELOG.md
+++ b/packages/go_router/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 3.1.1
+
+- Uses first match if there are more than one route to match. [ [#99833](https://github.com/flutter/flutter/issues/99833)
+
 ## 3.1.0
 
 - Added `GoRouteData` and `TypedGoRoute` to support `package:go_router_builder`.
diff --git a/packages/go_router/lib/src/go_route.dart b/packages/go_router/lib/src/go_route.dart
index a50831a..832c4e8 100644
--- a/packages/go_router/lib/src/go_route.dart
+++ b/packages/go_router/lib/src/go_route.dart
@@ -162,6 +162,30 @@
   ///   ],
   /// );
   ///
+  /// If there are multiple routes that match the location, the first match is used.
+  /// To make predefined routes to take precedence over dynamic routes eg. '/:id'
+  /// consider adding the dynamic route at the end of the routes
+  /// For example:
+  /// ```
+  /// final GoRouter _router = GoRouter(
+  ///   routes: <GoRoute>[
+  ///     GoRoute(
+  ///       path: '/',
+  ///       redirect: (_) => '/family/${Families.data[0].id}',
+  ///     ),
+  ///     GoRoute(
+  ///       path: '/family',
+  ///       pageBuilder: (BuildContext context, GoRouterState state) => ...,
+  ///     ),
+  ///     GoRoute(
+  ///       path: '/:username',
+  ///       pageBuilder: (BuildContext context, GoRouterState state) => ...,
+  ///     ),
+  ///   ],
+  /// );
+  /// ```
+  /// In the above example, if /family route is matched, it will be used.
+  /// else /:username route will be used.
   final List<GoRoute> routes;
 
   /// An optional redirect function for this route.
diff --git a/packages/go_router/lib/src/go_router_delegate.dart b/packages/go_router/lib/src/go_router_delegate.dart
index bb05e3c..f901a94 100644
--- a/packages/go_router/lib/src/go_router_delegate.dart
+++ b/packages/go_router/lib/src/go_router_delegate.dart
@@ -441,30 +441,10 @@
     ).toList();
 
     assert(matchStacks.isNotEmpty, 'no routes for location: $location');
-    assert(() {
-      if (matchStacks.length > 1) {
-        final StringBuffer sb = StringBuffer()
-          ..writeln('too many routes for location: $location');
 
-        for (final List<GoRouteMatch> stack in matchStacks) {
-          sb.writeln(
-              '\t${stack.map((GoRouteMatch m) => m.route.path).join(' => ')}');
-        }
-
-        assert(false, sb.toString());
-      }
-
-      assert(matchStacks.length == 1);
-      final GoRouteMatch match = matchStacks.first.last;
-      final String loc1 = _addQueryParams(match.subloc, match.queryParams);
-      final Uri uri2 = Uri.parse(location);
-      final String loc2 = _addQueryParams(uri2.path, uri2.queryParameters);
-
-      // NOTE: match the lower case, since subloc is canonicalized to match the
-      // path case whereas the location can be any case
-      assert(loc1.toLowerCase() == loc2.toLowerCase(), '$loc1 != $loc2');
-      return true;
-    }());
+    // If there are multiple routes that match the location, returning the first one.
+    // To make predefined routes to take precedence over dynamic routes eg. '/:id'
+    // consider adding the dynamic route at the end of the routes
 
     return matchStacks.first;
   }
diff --git a/packages/go_router/pubspec.yaml b/packages/go_router/pubspec.yaml
index 7b66096..552c431 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: 3.1.0
+version: 3.1.1
 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 bc0804e..8316597 100644
--- a/packages/go_router/test/go_router_test.dart
+++ b/packages/go_router/test/go_router_test.dart
@@ -39,7 +39,7 @@
       expect(router.screenFor(matches.first).runtimeType, HomeScreen);
     });
 
-    test('match too many routes', () {
+    test('If there is more than one route to match, use the first match', () {
       final List<GoRoute> routes = <GoRoute>[
         GoRoute(path: '/', builder: _dummy),
         GoRoute(path: '/', builder: _dummy),
@@ -50,7 +50,7 @@
       final List<GoRouteMatch> matches = router.routerDelegate.matches;
       expect(matches, hasLength(1));
       expect(matches.first.fullpath, '/');
-      expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
+      expect(router.screenFor(matches.first).runtimeType, DummyScreen);
     });
 
     test('empty path', () {
@@ -275,23 +275,32 @@
       }
     });
 
-    test('match too many sub-routes', () {
+    test('return first matching route if too many subroutes', () {
       final List<GoRoute> routes = <GoRoute>[
         GoRoute(
           path: '/',
-          builder: _dummy,
+          builder: (BuildContext context, GoRouterState state) =>
+              const HomeScreen(),
           routes: <GoRoute>[
             GoRoute(
               path: 'foo/bar',
-              builder: _dummy,
+              builder: (BuildContext context, GoRouterState state) =>
+                  const FamilyScreen(''),
+            ),
+            GoRoute(
+              path: 'bar',
+              builder: (BuildContext context, GoRouterState state) =>
+                  const Page1Screen(),
             ),
             GoRoute(
               path: 'foo',
-              builder: _dummy,
+              builder: (BuildContext context, GoRouterState state) =>
+                  const Page2Screen(),
               routes: <GoRoute>[
                 GoRoute(
                   path: 'bar',
-                  builder: _dummy,
+                  builder: (BuildContext context, GoRouterState state) =>
+                      const LoginScreen(),
                 ),
               ],
             ),
@@ -300,10 +309,20 @@
       ];
 
       final GoRouter router = _router(routes);
+      router.go('/bar');
+      List<GoRouteMatch> matches = router.routerDelegate.matches;
+      expect(matches, hasLength(2));
+      expect(router.screenFor(matches[1]).runtimeType, Page1Screen);
+
       router.go('/foo/bar');
-      final List<GoRouteMatch> matches = router.routerDelegate.matches;
-      expect(matches, hasLength(1));
-      expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
+      matches = router.routerDelegate.matches;
+      expect(matches, hasLength(2));
+      expect(router.screenFor(matches[1]).runtimeType, FamilyScreen);
+
+      router.go('/foo');
+      matches = router.routerDelegate.matches;
+      expect(matches, hasLength(2));
+      expect(router.screenFor(matches[1]).runtimeType, Page2Screen);
     });
 
     test('router state', () {
@@ -424,17 +443,19 @@
       expect(router.screenFor(matches.first).runtimeType, FamilyScreen);
     });
 
-    test('match too many routes, ignoring case', () {
+    test('If there is more than one route to match, use the first match.', () {
       final List<GoRoute> routes = <GoRoute>[
+        GoRoute(path: '/', builder: _dummy),
         GoRoute(path: '/page1', builder: _dummy),
-        GoRoute(path: '/PaGe1', builder: _dummy),
+        GoRoute(path: '/page1', builder: _dummy),
+        GoRoute(path: '/:ok', builder: _dummy),
       ];
 
       final GoRouter router = _router(routes);
-      router.go('/PAGE1');
+      router.go('/user');
       final List<GoRouteMatch> matches = router.routerDelegate.matches;
       expect(matches, hasLength(1));
-      expect(router.screenFor(matches.first).runtimeType, ErrorScreen);
+      expect(router.screenFor(matches.first).runtimeType, DummyScreen);
     });
   });