| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'package:collection/collection.dart'; |
| import 'package:flutter/widgets.dart'; |
| |
| import 'configuration.dart'; |
| import 'pages/custom_transition_page.dart'; |
| import 'path_utils.dart'; |
| import 'typedefs.dart'; |
| |
| /// The base class for [GoRoute] and [ShellRoute]. |
| /// |
| /// Routes are defined in a tree such that parent routes must match the |
| /// current location for their child route to be considered a match. For |
| /// example the location "/home/user/12" matches with parent route "/home" and |
| /// child route "user/:userId". |
| /// |
| /// To create sub-routes for a route, provide them as a [GoRoute] list |
| /// with the sub routes. |
| /// |
| /// For example these routes: |
| /// ``` |
| /// / => HomePage() |
| /// family/f1 => FamilyPage('f1') |
| /// person/p2 => PersonPage('f1', 'p2') ← showing this page, Back pops ↑ |
| /// ``` |
| /// |
| /// Can be represented as: |
| /// |
| /// ``` |
| /// final GoRouter _router = GoRouter( |
| /// routes: <GoRoute>[ |
| /// GoRoute( |
| /// path: '/', |
| /// pageBuilder: (BuildContext context, GoRouterState state) => MaterialPage<void>( |
| /// key: state.pageKey, |
| /// child: HomePage(families: Families.data), |
| /// ), |
| /// routes: <GoRoute>[ |
| /// GoRoute( |
| /// path: 'family/:fid', |
| /// pageBuilder: (BuildContext context, GoRouterState state) { |
| /// final Family family = Families.family(state.params['fid']!); |
| /// return MaterialPage<void>( |
| /// key: state.pageKey, |
| /// child: FamilyPage(family: family), |
| /// ); |
| /// }, |
| /// routes: <GoRoute>[ |
| /// GoRoute( |
| /// path: 'person/:pid', |
| /// pageBuilder: (BuildContext context, GoRouterState state) { |
| /// final Family family = Families.family(state.params['fid']!); |
| /// final Person person = family.person(state.params['pid']!); |
| /// return MaterialPage<void>( |
| /// key: state.pageKey, |
| /// child: PersonPage(family: family, person: person), |
| /// ); |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// ], |
| /// ), |
| /// ], |
| /// ); |
| /// |
| /// 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. |
| /// |
| /// See [Sub-routes](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/sub_routes.dart) |
| /// for a complete runnable example. |
| @immutable |
| abstract class RouteBase { |
| const RouteBase._({ |
| this.routes = const <RouteBase>[], |
| }); |
| |
| /// The list of child routes associated with this route. |
| final List<RouteBase> routes; |
| } |
| |
| /// A route that is displayed visually above the matching parent route using the |
| /// [Navigator]. |
| /// |
| /// The widget returned by [builder] is wrapped in [Page] and provided to the |
| /// root Navigator, the nearest ShellRoute ancestor's Navigator, or the |
| /// Navigator with a matching [parentNavigatorKey]. |
| /// |
| /// The Page depends on the application type: [MaterialPage] for |
| /// [MaterialApp], [CupertinoPage] for [CupertinoApp], or |
| /// [NoTransitionPage] for [WidgetsApp]. |
| class GoRoute extends RouteBase { |
| /// Constructs a [GoRoute]. |
| /// - [path] and [name] cannot be empty strings. |
| /// - One of either [builder] or [pageBuilder] must be provided. |
| GoRoute({ |
| required this.path, |
| this.name, |
| this.builder, |
| this.pageBuilder, |
| this.parentNavigatorKey, |
| this.redirect, |
| List<RouteBase> routes = const <RouteBase>[], |
| }) : assert(path.isNotEmpty, 'GoRoute path cannot be empty'), |
| assert(name == null || name.isNotEmpty, 'GoRoute name cannot be empty'), |
| assert(pageBuilder != null || builder != null || redirect != null, |
| 'builder, pageBuilder, or redirect must be provided'), |
| super._( |
| routes: routes, |
| ) { |
| // cache the path regexp and parameters |
| _pathRE = patternToRegExp(path, _pathParams); |
| assert(() { |
| // check path params |
| final Map<String, List<String>> groupedParams = |
| _pathParams.groupListsBy<String>((String p) => p); |
| final Map<String, List<String>> dupParams = |
| Map<String, List<String>>.fromEntries( |
| groupedParams.entries |
| .where((MapEntry<String, List<String>> e) => e.value.length > 1), |
| ); |
| assert(dupParams.isEmpty, |
| 'duplicate path params: ${dupParams.keys.join(', ')}'); |
| |
| // check sub-routes |
| for (final RouteBase route in routes) { |
| // check paths |
| if (route is GoRoute) { |
| assert( |
| route.path == '/' || |
| (!route.path.startsWith('/') && !route.path.endsWith('/')), |
| 'sub-route path may not start or end with /: ${route.path}'); |
| } |
| } |
| return true; |
| }()); |
| } |
| |
| /// Optional name of the route. |
| /// |
| /// If used, a unique string name must be provided and it can not be empty. |
| /// |
| /// This is used in [GoRouter.namedLocation] and its related API. This |
| /// property can be used to navigate to this route without knowing exact the |
| /// URI of it. |
| /// |
| /// {@tool snippet} |
| /// Typical usage is as follows: |
| /// |
| /// ```dart |
| /// GoRoute( |
| /// name: 'home', |
| /// path: '/', |
| /// builder: (BuildContext context, GoRouterState state) => |
| /// HomeScreen(), |
| /// routes: <GoRoute>[ |
| /// GoRoute( |
| /// name: 'family', |
| /// path: 'family/:fid', |
| /// builder: (BuildContext context, GoRouterState state) => |
| /// FamilyScreen(), |
| /// ), |
| /// ], |
| /// ); |
| /// |
| /// context.go( |
| /// context.namedLocation('family'), |
| /// params: <String, String>{'fid': 123}, |
| /// queryParams: <String, String>{'qid': 'quid'}, |
| /// ); |
| /// ``` |
| /// |
| /// See the [named routes example](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/named_routes.dart) |
| /// for a complete runnable app. |
| final String? name; |
| |
| /// The path of this go route. |
| /// |
| /// For example: |
| /// ``` |
| /// GoRoute( |
| /// path: '/', |
| /// pageBuilder: (BuildContext context, GoRouterState state) => MaterialPage<void>( |
| /// key: state.pageKey, |
| /// child: HomePage(families: Families.data), |
| /// ), |
| /// ), |
| /// ``` |
| /// |
| /// The path also support path parameters. For a path: `/family/:fid`, it |
| /// matches all URIs start with `/family/...`, e.g. `/family/123`, |
| /// `/family/456` and etc. The parameter values are stored in [GoRouterState] |
| /// that are passed into [pageBuilder] and [builder]. |
| /// |
| /// The query parameter are also capture during the route parsing and stored |
| /// in [GoRouterState]. |
| /// |
| /// See [Query parameters and path parameters](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/sub_routes.dart) |
| /// to learn more about parameters. |
| final String path; |
| |
| /// A page builder for this route. |
| /// |
| /// Typically a MaterialPage, as in: |
| /// ``` |
| /// GoRoute( |
| /// path: '/', |
| /// pageBuilder: (BuildContext context, GoRouterState state) => MaterialPage<void>( |
| /// key: state.pageKey, |
| /// child: HomePage(families: Families.data), |
| /// ), |
| /// ), |
| /// ``` |
| /// |
| /// You can also use CupertinoPage, and for a custom page builder to use |
| /// custom page transitions, you can use [CustomTransitionPage]. |
| final GoRouterPageBuilder? pageBuilder; |
| |
| /// A custom builder for this route. |
| /// |
| /// For example: |
| /// ``` |
| /// GoRoute( |
| /// path: '/', |
| /// builder: (BuildContext context, GoRouterState state) => FamilyPage( |
| /// families: Families.family( |
| /// state.params['id'], |
| /// ), |
| /// ), |
| /// ), |
| /// ``` |
| /// |
| final GoRouterWidgetBuilder? builder; |
| |
| /// An optional redirect function for this route. |
| /// |
| /// In the case that you like to make a redirection decision for a specific |
| /// route (or sub-route), consider doing so by passing a redirect function to |
| /// the GoRoute constructor. |
| /// |
| /// For example: |
| /// ``` |
| /// final GoRouter _router = GoRouter( |
| /// routes: <GoRoute>[ |
| /// GoRoute( |
| /// path: '/', |
| /// redirect: (_) => '/family/${Families.data[0].id}', |
| /// ), |
| /// GoRoute( |
| /// path: '/family/:fid', |
| /// pageBuilder: (BuildContext context, GoRouterState state) => ..., |
| /// ), |
| /// ], |
| /// ); |
| /// ``` |
| /// |
| /// If there are multiple redirects in the matched routes, the parent route's |
| /// redirect takes priority over sub-route's. |
| /// |
| /// For example: |
| /// ``` |
| /// final GoRouter _router = GoRouter( |
| /// routes: <GoRoute>[ |
| /// GoRoute( |
| /// path: '/', |
| /// redirect: (_) => '/page1', // this takes priority over the sub-route. |
| /// routes: <GoRoute>[ |
| /// GoRoute( |
| /// path: 'child', |
| /// redirect: (_) => '/page2', |
| /// ), |
| /// ], |
| /// ), |
| /// ], |
| /// ); |
| /// ``` |
| /// |
| /// The `context.go('/child')` will be redirected to `/page1` instead of |
| /// `/page2`. |
| /// |
| /// Redirect can also be used for conditionally preventing users from visiting |
| /// routes, also known as route guards. One canonical example is user |
| /// authentication. See [Redirection](https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart) |
| /// for a complete runnable example. |
| /// |
| /// If [BuildContext.dependOnInheritedWidgetOfExactType] is used during the |
| /// redirection (which is how `of` method is usually implemented), a |
| /// re-evaluation will be triggered if the [InheritedWidget] changes. |
| final GoRouterRedirect? redirect; |
| |
| /// An optional key specifying which Navigator to display this route's screen |
| /// onto. |
| /// |
| /// Specifying the root Navigator will stack this route onto that |
| /// Navigator instead of the nearest ShellRoute ancestor. |
| final GlobalKey<NavigatorState>? parentNavigatorKey; |
| |
| /// Match this route against a location. |
| RegExpMatch? matchPatternAsPrefix(String loc) => |
| _pathRE.matchAsPrefix(loc) as RegExpMatch?; |
| |
| /// Extract the path parameters from a match. |
| Map<String, String> extractPathParams(RegExpMatch match) => |
| extractPathParameters(_pathParams, match); |
| |
| final List<String> _pathParams = <String>[]; |
| |
| late final RegExp _pathRE; |
| } |
| |
| /// A route that displays a UI shell around the matching child route. |
| /// |
| /// When a ShellRoute is added to the list of routes on GoRouter or GoRoute, a |
| /// new Navigator that is used to display any matching sub-routes, instead of |
| /// placing them on the root Navigator. |
| /// |
| /// To display a child route on a different Navigator, provide it with a |
| /// [parentNavigatorKey] that matches the key provided to either the [GoRouter] |
| /// or [ShellRoute] constructor. In this example, the _rootNavigator key is |
| /// passed to the /b/details route so that it displays on the root Navigator |
| /// instead of the ShellRoute's Navigator: |
| /// |
| /// ``` |
| /// final GlobalKey<NavigatorState> _rootNavigatorKey = |
| // GlobalKey<NavigatorState>(); |
| /// |
| /// final GoRouter _router = GoRouter( |
| /// navigatorKey: _rootNavigatorKey, |
| /// initialLocation: '/a', |
| /// routes: [ |
| /// ShellRoute( |
| /// navigatorKey: _shellNavigatorKey, |
| /// builder: (context, state, child) { |
| /// return ScaffoldWithNavBar(child: child); |
| /// }, |
| /// routes: [ |
| /// // This screen is displayed on the ShellRoute's Navigator. |
| /// GoRoute( |
| /// path: '/a', |
| /// builder: (context, state) { |
| /// return const ScreenA(); |
| /// }, |
| /// routes: <RouteBase>[ |
| /// // This screen is displayed on the ShellRoute's Navigator. |
| /// GoRoute( |
| /// path: 'details', |
| /// builder: (BuildContext context, GoRouterState state) { |
| /// return const DetailsScreen(label: 'A'); |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// // Displayed ShellRoute's Navigator. |
| /// GoRoute( |
| /// path: '/b', |
| /// builder: (BuildContext context, GoRouterState state) { |
| /// return const ScreenB(); |
| /// }, |
| /// routes: <RouteBase>[ |
| /// // Displayed on the root Navigator by specifying the |
| /// // [parentNavigatorKey]. |
| /// GoRoute( |
| /// path: 'details', |
| /// parentNavigatorKey: _rootNavigatorKey, |
| /// builder: (BuildContext context, GoRouterState state) { |
| /// return const DetailsScreen(label: 'B'); |
| /// }, |
| /// ), |
| /// ], |
| /// ), |
| /// ], |
| /// ), |
| /// ], |
| /// ); |
| /// ``` |
| /// |
| /// The widget built by the matching sub-route becomes the child parameter |
| /// of the [builder]. |
| /// |
| /// For example: |
| /// |
| /// ``` |
| /// ShellRoute( |
| /// path: '/', |
| /// builder: (BuildContext context, GoRouterState state, Widget child) { |
| /// return Scaffold( |
| /// appBar: AppBar( |
| /// title: Text('App Shell') |
| /// ), |
| /// body: Center( |
| /// child: child, |
| /// ), |
| /// ); |
| /// }, |
| /// routes: [ |
| /// GoRoute( |
| /// path: 'a' |
| /// builder: (BuildContext context, GoRouterState state) { |
| /// return Text('Child Route "/a"'); |
| /// } |
| /// ), |
| /// ], |
| /// ), |
| /// ``` |
| /// |
| class ShellRoute extends RouteBase { |
| /// Constructs a [ShellRoute]. |
| ShellRoute({ |
| this.builder, |
| this.pageBuilder, |
| super.routes, |
| GlobalKey<NavigatorState>? navigatorKey, |
| }) : assert(routes.isNotEmpty), |
| navigatorKey = navigatorKey ?? GlobalKey<NavigatorState>(), |
| super._() { |
| for (final RouteBase route in routes) { |
| assert(route is GoRoute); |
| assert((route as GoRoute).parentNavigatorKey == null); |
| } |
| } |
| |
| /// The widget builder for a shell route. |
| /// |
| /// Similar to GoRoute builder, but with an additional child parameter. This |
| /// child parameter is the Widget built by calling the matching sub-route's |
| /// builder. |
| final ShellRouteBuilder? builder; |
| |
| /// The page builder for a shell route. |
| /// |
| /// Similar to GoRoute pageBuilder, but with an additional child parameter. |
| /// This child parameter is the Widget built by calling the matching |
| /// sub-route's builder. |
| final ShellRoutePageBuilder? pageBuilder; |
| |
| /// The [GlobalKey] to be used by the [Navigator] built for this route. |
| /// All ShellRoutes build a Navigator by default. Child GoRoutes |
| /// are placed onto this Navigator instead of the root Navigator. |
| final GlobalKey<NavigatorState> navigatorKey; |
| } |