blob: 755aed67078a9fcedb67da0ce31b13cab9ac6b2a [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 'configuration.dart';
import 'match.dart';
import 'path_utils.dart';
/// Converts a location into a list of [RouteMatch] objects.
class RouteMatcher {
/// [RouteMatcher] constructor.
/// The route configuration.
final RouteConfiguration configuration;
/// Finds the routes that matched the given URL.
RouteMatchList findMatch(String location, {Object? extra}) {
final String canonicalLocation = canonicalUri(location);
final List<RouteMatch> matches =
_getLocRouteMatches(canonicalLocation, extra);
return RouteMatchList(matches);
List<RouteMatch> _getLocRouteMatches(String location, Object? extra) {
final Uri uri = Uri.parse(location);
final List<RouteMatch> result = _getLocRouteRecursively(
loc: uri.path,
restLoc: uri.path,
routes: configuration.routes,
parentFullpath: '',
parentSubloc: '',
queryParams: uri.queryParameters,
extra: extra,
if (result.isEmpty) {
throw MatcherError('no routes for location', location);
return result;
/// The list of [RouteMatch] objects.
class RouteMatchList {
/// RouteMatchList constructor.
/// Constructs an empty matches object.
factory RouteMatchList.empty() => RouteMatchList(<RouteMatch>[]);
final List<RouteMatch> _matches;
/// Returns true if there are no matches.
bool get isEmpty => _matches.isEmpty;
/// Returns true if there are matches.
bool get isNotEmpty => _matches.isNotEmpty;
/// The original URL that was matched.
Uri get location =>
_matches.isEmpty ? Uri() : Uri.parse(_matches.last.fullUriString);
/// Pushes a match onto the list of matches.
void push(RouteMatch match) {
/// Removes the last match.
void pop() {
'You have popped the last page off of the stack,'
' there are no pages left to show');
/// Returns true if [pop] can safely be called.
bool canPop() {
return _matches.length > 1;
/// An optional object provided by the app during navigation.
Object? get extra => _matches.last.extra;
/// The last matching route.
RouteMatch get last => _matches.last;
/// The route matches.
List<RouteMatch> get matches => _matches;
/// Returns true if the current match intends to display an error screen.
bool get isError => matches.length == 1 && matches.first.error != null;
/// Returns the error that this match intends to display.
Exception? get error => matches.first.error;
/// An error that occurred during matching.
class MatcherError extends Error {
/// Constructs a [MatcherError].
MatcherError(String message, this.location)
: message = message + ': $location';
/// The error message.
final String message;
/// The location that failed to match.
final String location;
String toString() {
return message;
List<RouteMatch> _getLocRouteRecursively({
required String loc,
required String restLoc,
required String parentSubloc,
required List<GoRoute> routes,
required String parentFullpath,
required Map<String, String> queryParams,
required Object? extra,
}) {
bool debugGatherAllMatches = false;
assert(() {
debugGatherAllMatches = true;
return true;
final List<List<RouteMatch>> result = <List<RouteMatch>>[];
// find the set of matches at this level of the tree
for (final GoRoute route in routes) {
final String fullpath = concatenatePaths(parentFullpath, route.path);
final RouteMatch? match = RouteMatch.match(
route: route,
restLoc: restLoc,
parentSubloc: parentSubloc,
fullpath: fullpath,
queryParams: queryParams,
extra: extra,
if (match == null) {
if (match.subloc.toLowerCase() == loc.toLowerCase()) {
// If it is a complete match, then return the matched route
// NOTE: need a lower case match because subloc is canonicalized to match
// the path case whereas the location can be of any case and still match
} else if (route.routes.isEmpty) {
// If it is partial match but no sub-routes, bail.
} else {
// Otherwise, recurse
final String childRestLoc =
loc.substring(match.subloc.length + (match.subloc == '/' ? 0 : 1));
final List<RouteMatch> subRouteMatch = _getLocRouteRecursively(
loc: loc,
restLoc: childRestLoc,
parentSubloc: match.subloc,
routes: route.routes,
parentFullpath: fullpath,
queryParams: queryParams,
extra: extra,
// If there's no sub-route matches, there is no match for this location
if (subRouteMatch.isEmpty) {
result.add(<RouteMatch>[match, ...subRouteMatch]);
// Should only reach here if there is a match.
if (debugGatherAllMatches) {
} else {
if (result.isEmpty) {
return <RouteMatch>[];
// 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 result.first;