blob: 832c4e8ae8746574b5d9e21fbec255a3483f6e20 [file] [log] [blame]
// 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 'custom_transition_page.dart';
import 'go_router_state.dart';
import 'path_parser.dart';
import 'typedefs.dart';
/// A declarative mapping between a route path and a page builder.
class GoRoute {
/// Default constructor used to create mapping between a
/// route path and a page builder.
GoRoute({
required this.path,
this.name,
this.pageBuilder,
this.builder = _invalidBuilder,
this.routes = const <GoRoute>[],
this.redirect = _noRedirection,
}) : assert(path.isNotEmpty, 'GoRoute path cannot be empty'),
assert(name == null || name.isNotEmpty, 'GoRoute name cannot be empty'),
assert(
pageBuilder != null ||
builder != _invalidBuilder ||
redirect != _noRedirection,
'GoRoute builder parameter not set\nSee gorouter.dev/redirection#considerations for details') {
// 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 GoRoute route in routes) {
// check paths
assert(
route.path == '/' ||
(!route.path.startsWith('/') && !route.path.endsWith('/')),
'sub-route path may not start or end with /: ${route.path}');
}
return true;
}());
}
final List<String> _pathParams = <String>[];
late final RegExp _pathRE;
/// Optional name of the route.
///
/// If used, a unique string name must be provided and it can not be empty.
final String? name;
/// The path of this go route.
///
/// For example in:
/// ```
/// GoRoute(
/// path: '/',
/// pageBuilder: (BuildContext context, GoRouterState state) => MaterialPage<void>(
/// key: state.pageKey,
/// child: HomePage(families: Families.data),
/// ),
/// ),
/// ```
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;
/// A list of sub go routes for this route.
///
/// 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.
final List<GoRoute> routes;
/// 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), you can do 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) => ...,
/// ),
/// ],
/// );
/// ```
final GoRouterRedirect redirect;
/// 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);
static String? _noRedirection(GoRouterState state) => null;
static Widget _invalidBuilder(
BuildContext context,
GoRouterState state,
) =>
const SizedBox.shrink();
}