Skip to content

[go_router] Support for top level onEnter callback. #8339

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 38 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
6be9a5d
[go_router] Added top level onEnter callback.
omar-hanafy Dec 22, 2024
171b639
added version 14.7.0
omar-hanafy Dec 22, 2024
f52a269
Merge branch 'main' into main
omar-hanafy Dec 25, 2024
3bbd241
Merge branch 'main' into main
omar-hanafy Dec 27, 2024
6a60006
Merge branch 'main' into main
omar-hanafy Jan 3, 2025
d1e1fc2
[go_router] added nextState, and currentState to OnEnter signature, a…
omar-hanafy Jan 24, 2025
516db13
Merge branch 'main' into main
omar-hanafy Jan 24, 2025
e1f10b1
Merge branch 'main' into main
omar-hanafy Jan 26, 2025
7a847b8
Merge branch 'main' into main
omar-hanafy Jan 28, 2025
b08d804
Merge branch 'main' into main
omar-hanafy Feb 2, 2025
1e25466
Merge branch 'main' into main
omar-hanafy Feb 4, 2025
aec8e47
Add router instance to OnEnter callback
omar-hanafy Feb 4, 2025
2bdc147
Merge branch 'main' into main
omar-hanafy Feb 8, 2025
1bd3c18
[go_router] Async onEnter, improved redirection, and loop prevention.
omar-hanafy Feb 15, 2025
61729b2
Merge branch 'main' into main
omar-hanafy Feb 15, 2025
8334a64
Merge branch 'main' into main
omar-hanafy Feb 17, 2025
f28337e
improved redirection and async handling.
omar-hanafy Feb 18, 2025
4092405
extracting the onEnter logic into its own helper class.
omar-hanafy Feb 18, 2025
c1c09d0
added named params to handleTopOnEnter.
omar-hanafy Feb 18, 2025
d9e6ea6
move tests
cedvdb Feb 20, 2025
07c15f0
Merge pull request #3 from cedvdb/move_tests
omar-hanafy Feb 22, 2025
3fbe011
Merge branch 'main' into main
omar-hanafy Mar 28, 2025
67df52a
added tests
cedvdb Apr 6, 2025
eef39b1
Merge pull request #4 from cedvdb/omar-add-on-enter-tests
omar-hanafy Apr 7, 2025
359eb0e
Merge branch 'flutter:main' into main
omar-hanafy Apr 7, 2025
cc57519
[go_router] Fix onEnter callback exception handling and enhance tests
omar-hanafy Apr 8, 2025
d4f2416
[go_router] updated Should allow redirection with query parameters te…
omar-hanafy Apr 9, 2025
56f2dbe
Merge branch 'main' into main
omar-hanafy Apr 16, 2025
c458982
Merge branch 'main' into main
omar-hanafy Apr 28, 2025
921dcb3
Merge branch 'main' into main
omar-hanafy May 30, 2025
757f5a1
[go_router] Use specific imports in on_enter.dart
omar-hanafy May 30, 2025
b5e1e9e
Merge branch 'main' of https://github.com/omar-hanafy/packages
omar-hanafy May 30, 2025
86c506b
Merge branch 'main' into main
omar-hanafy Jun 10, 2025
3c4a85f
Merge branch 'main' into main
omar-hanafy Jun 11, 2025
9d52c0d
Merge branch 'main' into main
omar-hanafy Jun 18, 2025
97c5ed8
Merge branch 'main' into main
omar-hanafy Jun 24, 2025
0323a45
Merge branch 'main' into main
omar-hanafy Jul 9, 2025
4a9e6ff
[go_router] Replace boolean return with sealed class API for onEnter
omar-hanafy Jul 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 16.1.0

- Adds new top level `onEnter` callback with access to current and next route states.
- Deprecates top level `redirect` in favor of `onEnter`.

## 16.0.0

- **BREAKING CHANGE**
@@ -1199,4 +1204,3 @@
## 0.1.0

- squatting on the package name (I'm not too proud to admit it)

440 changes: 440 additions & 0 deletions packages/go_router/example/lib/top_level_on_enter.dart

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions packages/go_router/lib/go_router.dart
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ export 'src/match.dart' hide RouteMatchListCodec;
export 'src/misc/errors.dart';
export 'src/misc/extensions.dart';
export 'src/misc/inherited_router.dart';
export 'src/on_enter.dart' show Allow, Block, OnEnter, OnEnterResult;
export 'src/pages/custom_transition_page.dart';
export 'src/parser.dart';
export 'src/route.dart';
13 changes: 11 additions & 2 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
@@ -11,14 +11,17 @@ import 'package:flutter/widgets.dart';
import 'logging.dart';
import 'match.dart';
import 'misc/errors.dart';
import 'on_enter.dart';
import 'path_utils.dart';
import 'route.dart';
import 'router.dart';
import 'state.dart';

/// The signature of the redirect callback.
typedef GoRouterRedirect = FutureOr<String?> Function(
BuildContext context, GoRouterState state);
BuildContext context,
GoRouterState state,
);

typedef _NamedPath = ({String path, bool caseSensitive});

@@ -54,7 +57,9 @@ class RouteConfiguration {
// Check that each parentNavigatorKey refers to either a ShellRoute's
// navigatorKey or the root navigator key.
static bool _debugCheckParentNavigatorKeys(
List<RouteBase> routes, List<GlobalKey<NavigatorState>> allowedKeys) {
List<RouteBase> routes,
List<GlobalKey<NavigatorState>> allowedKeys,
) {
for (final RouteBase route in routes) {
if (route is GoRoute) {
final GlobalKey<NavigatorState>? parentKey = route.parentNavigatorKey;
@@ -217,6 +222,7 @@ class RouteConfiguration {
extra: matchList.extra,
pageKey: const ValueKey<String>('topLevel'),
topRoute: matchList.lastOrNull?.route,
error: matchList.error,
);
}

@@ -229,6 +235,9 @@ class RouteConfiguration {
/// Top level page redirect.
GoRouterRedirect get topRedirect => _routingConfig.value.redirect;

/// Top level page on enter.
OnEnter? get topOnEnter => _routingConfig.value.onEnter;

/// The limit for the number of consecutive redirects.
int get redirectLimit => _routingConfig.value.redirectLimit;

307 changes: 307 additions & 0 deletions packages/go_router/lib/src/on_enter.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,307 @@
// ignore_for_file: use_build_context_synchronously

// 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 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'configuration.dart';
import 'information_provider.dart';
import 'match.dart';
import 'misc/errors.dart';
import 'parser.dart';
import 'router.dart';
import 'state.dart';

/// The result of an [onEnter] callback.
///
/// This sealed class represents the possible outcomes of navigation interception.
sealed class OnEnterResult {
Copy link
Contributor Author

@omar-hanafy omar-hanafy Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this should probably be abstract.

Why it should be abstract ? I thought it would be better to allow this class to be extended only through the package, since we only support allow and block!

It would also be helpful for discoverability if static members allow and block would be added

I do not get that, can u clarify what u want here.

Copy link
Contributor

@cedvdb cedvdb Jul 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes right

Just a suggestion, this helps with discoverability, but since the documentation is already pretty clear, it may not be necessary

static OnEnterResult allow() => Allow();
static OnEnterResult block() => Block()

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean u want that static methods to be inside the OnEnterResult, so users be able to OnEnterResult.allow() or OnEnterResult.block()? okay good one, @chunhtai what do you think ?

Copy link
Contributor

@cedvdb cedvdb Jul 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you mark this as resolved ? Forget about my suggestion, this can be done later if necessary. Let's avoid noise because back and forth take weeks

/// Creates an [OnEnterResult].
const OnEnterResult();
}

/// Allows the navigation to proceed.
final class Allow extends OnEnterResult {
/// Creates an [Allow] result.
const Allow();
}

/// Blocks the navigation from proceeding.
final class Block extends OnEnterResult {
/// Creates a [Block] result.
const Block();
}

/// The signature for the top-level [onEnter] callback.
///
/// This callback receives the [BuildContext], the current navigation state,
/// the state being navigated to, and a reference to the [GoRouter] instance.
/// It returns a [Future<OnEnterResult>] which should resolve to [Allow] if navigation
/// is allowed, or [Block] to block navigation.
typedef OnEnter = Future<OnEnterResult> Function(
BuildContext context,
GoRouterState currentState,
GoRouterState nextState,
GoRouter goRouter,
);

/// Handles the top-level [onEnter] callback logic and manages redirection history.
///
/// This class encapsulates the logic to execute the top-level [onEnter] callback,
/// enforce the redirection limit defined in the router configuration, and generate
/// an error match list when the limit is exceeded. It is used internally by [GoRouter]
/// during route parsing.
class OnEnterHandler {
/// Creates an [OnEnterHandler] instance.
///
/// * [configuration] is the current route configuration containing all route definitions.
/// * [router] is the [GoRouter] instance used for navigation actions.
/// * [onParserException] is an optional exception handler invoked on route parsing errors.
OnEnterHandler({
required RouteConfiguration configuration,
required GoRouter router,
required ParserExceptionHandler? onParserException,
}) : _onParserException = onParserException,
_configuration = configuration,
_router = router;

/// The current route configuration.
///
/// Contains all route definitions, redirection logic, and navigation settings.
final RouteConfiguration _configuration;

/// Optional exception handler for route parsing errors.
///
/// This handler is invoked when errors occur during route parsing (for example,
/// when the [onEnter] redirection limit is exceeded) to return a fallback [RouteMatchList].
final ParserExceptionHandler? _onParserException;

/// The [GoRouter] instance used to perform navigation actions.
///
/// This provides access to the imperative navigation methods (like [go], [push],
/// [replace], etc.) and serves as a fallback reference in case the [BuildContext]
/// does not include a [GoRouter].
final GoRouter _router;

/// A history of URIs encountered during [onEnter] redirections.
///
/// This list tracks every URI that triggers an [onEnter] redirection, ensuring that
/// the number of redirections does not exceed the limit defined in the router's configuration.
final List<Uri> _redirectionHistory = <Uri>[];

/// Executes the top-level [onEnter] callback and determines whether navigation should proceed.
///
/// It checks for redirection errors by verifying if the redirection history exceeds the
/// configured limit. If everything is within limits, this method builds the current and
/// next navigation states, then executes the [onEnter] callback.
///
/// * If [onEnter] returns [Allow], the [onCanEnter] callback is invoked to allow navigation.
/// * If [onEnter] returns [Block], the [onCanNotEnter] callback is invoked to block navigation.
///
/// Exceptions thrown synchronously or asynchronously by [onEnter] are caught and processed
/// via the [_onParserException] handler if available.
///
/// Returns a [Future<RouteMatchList>] representing the final navigation state.
Future<RouteMatchList> handleTopOnEnter({
required BuildContext context,
required RouteInformation routeInformation,
required RouteInformationState<dynamic> infoState,
required Future<RouteMatchList> Function() onCanEnter,
required Future<RouteMatchList> Function() onCanNotEnter,
}) {
final OnEnter? topOnEnter = _configuration.topOnEnter;
// If no onEnter is configured, allow navigation immediately.
if (topOnEnter == null) {
return onCanEnter();
}

// Check if the redirection history exceeds the configured limit.
final RouteMatchList? redirectionErrorMatchList =
_redirectionErrorMatchList(context, routeInformation.uri, infoState);

if (redirectionErrorMatchList != null) {
// Return immediately if the redirection limit is exceeded.
return SynchronousFuture<RouteMatchList>(redirectionErrorMatchList);
}

// Find route matches for the incoming URI.
final RouteMatchList incomingMatches = _configuration.findMatch(
routeInformation.uri,
extra: infoState.extra,
);

// Build the next navigation state.
final GoRouterState nextState =
_buildTopLevelGoRouterState(incomingMatches);

// Get the current state from the router delegate.
final RouteMatchList currentMatchList =
_router.routerDelegate.currentConfiguration;
final GoRouterState currentState = currentMatchList.isNotEmpty
? _buildTopLevelGoRouterState(currentMatchList)
: nextState;

// Execute the onEnter callback in a try-catch to capture synchronous exceptions.
Future<OnEnterResult> onEnterResultFuture;
try {
onEnterResultFuture = topOnEnter(
context,
currentState,
nextState,
_router,
);
} catch (error) {
final RouteMatchList errorMatchList = _errorRouteMatchList(
routeInformation.uri,
error is GoException ? error : GoException(error.toString()),
extra: infoState.extra,
);

_resetRedirectionHistory();

return SynchronousFuture<RouteMatchList>(_onParserException != null
? _onParserException(context, errorMatchList)
: errorMatchList);
}

// Reset the redirection history after attempting the callback.
_resetRedirectionHistory();

// Handle asynchronous completion and catch any errors.
return onEnterResultFuture.then<RouteMatchList>(
(OnEnterResult result) {
if (result is Allow) {
return onCanEnter();
} else if (result is Block) {
return onCanNotEnter();
} else {
// This should never happen with a sealed class, but provide a fallback
throw GoException(
'Invalid OnEnterResult type: ${result.runtimeType}');
}
},
onError: (Object error, StackTrace stackTrace) {
final RouteMatchList errorMatchList = _errorRouteMatchList(
routeInformation.uri,
error is GoException ? error : GoException(error.toString()),
extra: infoState.extra,
);

return _onParserException != null
? _onParserException(context, errorMatchList)
: errorMatchList;
},
);
}

/// Builds a [GoRouterState] based on the given [matchList].
///
/// This method derives the effective URI, full path, path parameters, and extra data from
/// the topmost route match, drilling down through nested shells if necessary.
///
/// Returns a constructed [GoRouterState] reflecting the current or next navigation state.
GoRouterState _buildTopLevelGoRouterState(RouteMatchList matchList) {
// Determine effective navigation state from the match list.
Uri effectiveUri = matchList.uri;
String? effectiveFullPath = matchList.fullPath;
Map<String, String> effectivePathParams = matchList.pathParameters;
String effectiveMatchedLocation = matchList.uri.path;
Object? effectiveExtra = matchList.extra; // Base extra

if (matchList.matches.isNotEmpty) {
RouteMatchBase lastMatch = matchList.matches.last;
// Drill down to the actual leaf match even inside shell routes.
while (lastMatch is ShellRouteMatch) {
if (lastMatch.matches.isEmpty) {
break;
}
lastMatch = lastMatch.matches.last;
}

if (lastMatch is ImperativeRouteMatch) {
// Use state from the imperative match.
effectiveUri = lastMatch.matches.uri;
effectiveFullPath = lastMatch.matches.fullPath;
effectivePathParams = lastMatch.matches.pathParameters;
effectiveMatchedLocation = lastMatch.matches.uri.path;
effectiveExtra = lastMatch.matches.extra;
} else {
// For non-imperative matches, use the matched location and extra from the match list.
effectiveMatchedLocation = lastMatch.matchedLocation;
effectiveExtra = matchList.extra;
}
}

return GoRouterState(
_configuration,
uri: effectiveUri,
matchedLocation: effectiveMatchedLocation,
name: matchList.lastOrNull?.route.name,
path: matchList.lastOrNull?.route.path,
fullPath: effectiveFullPath,
pathParameters: effectivePathParams,
extra: effectiveExtra,
pageKey: const ValueKey<String>('topLevel'),
topRoute: matchList.lastOrNull?.route,
error: matchList.error,
);
}

/// Processes the redirection history and checks against the configured redirection limit.
///
/// Adds [redirectedUri] to the history and, if the limit is exceeded, returns an error
/// match list. Otherwise, returns null.
RouteMatchList? _redirectionErrorMatchList(
BuildContext context,
Uri redirectedUri,
RouteInformationState<dynamic> infoState,
) {
_redirectionHistory.add(redirectedUri);
if (_redirectionHistory.length > _configuration.redirectLimit) {
final String formattedHistory =
_formatOnEnterRedirectionHistory(_redirectionHistory);
final RouteMatchList errorMatchList = _errorRouteMatchList(
redirectedUri,
GoException('Too many onEnter calls detected: $formattedHistory'),
extra: infoState.extra,
);
_resetRedirectionHistory();
return _onParserException != null
? _onParserException(context, errorMatchList)
: errorMatchList;
}
return null;
}

/// Clears the redirection history.
void _resetRedirectionHistory() {
_redirectionHistory.clear();
}

/// Formats the redirection history into a string for error reporting.
String _formatOnEnterRedirectionHistory(List<Uri> history) {
return history.map((Uri uri) => uri.toString()).join(' => ');
}

/// Creates an error [RouteMatchList] for the given [uri] and [exception].
///
/// This is used to encapsulate errors encountered during redirection or parsing.
static RouteMatchList _errorRouteMatchList(
Uri uri,
GoException exception, {
Object? extra,
}) {
return RouteMatchList(
matches: const <RouteMatch>[],
extra: extra,
error: exception,
uri: uri,
pathParameters: const <String, String>{},
);
}
}
173 changes: 117 additions & 56 deletions packages/go_router/lib/src/parser.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// go_route_information_parser.dart
// ignore_for_file: use_build_context_synchronously
// Copyright 2013 The Flutter Authors.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

@@ -8,11 +10,10 @@ import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'configuration.dart';
import 'information_provider.dart';
import '../go_router.dart';
import 'logging.dart';
import 'match.dart';
import 'router.dart';
import 'on_enter.dart';

/// The function signature of [GoRouteInformationParser.onParserException].
///
@@ -27,108 +28,158 @@ typedef ParserExceptionHandler = RouteMatchList Function(
);

/// Converts between incoming URLs and a [RouteMatchList] using [RouteMatcher].
/// Also performs redirection using [RouteRedirector].
///
/// Also performs redirection using [RouteRedirector] and integrates the top-level
/// onEnter logic via [OnEnterHandler].
class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
/// Creates a [GoRouteInformationParser].
GoRouteInformationParser({
required this.configuration,
required String? initialLocation,
required GoRouter router,
required this.onParserException,
}) : _routeMatchListCodec = RouteMatchListCodec(configuration);
}) : _routeMatchListCodec = RouteMatchListCodec(configuration),
_initialLocation = initialLocation,
_onEnterHandler = OnEnterHandler(
configuration: configuration,
router: router,
onParserException: onParserException,
);

/// The route configuration used for parsing [RouteInformation]s.
final RouteConfiguration configuration;

/// The exception handler that is called when parser can't handle the incoming
/// uri.
///
/// This method must return a [RouteMatchList] for the parsed result.
/// Exception handler for parser errors.
final ParserExceptionHandler? onParserException;

final RouteMatchListCodec _routeMatchListCodec;

final Random _random = Random();
final String? _initialLocation;

/// Stores the last successful match list to enable "stay" on the same route.
RouteMatchList? _lastMatchList;

/// The future of current route parsing.
///
/// This is used for testing asynchronous redirection.
/// Instance of [OnEnterHandler] to process top-level onEnter logic.
final OnEnterHandler _onEnterHandler;

/// The future of current route parsing (used for testing asynchronous redirection).
@visibleForTesting
Future<RouteMatchList>? debugParserFuture;

/// Called by the [Router]. The
final Random _random = Random();

@override
Future<RouteMatchList> parseRouteInformationWithDependencies(
RouteInformation routeInformation,
BuildContext context,
) {
assert(routeInformation.state != null);
final Object state = routeInformation.state!;
// Safety check: if no state is provided, return an empty match list.
if (routeInformation.state == null) {
return SynchronousFuture<RouteMatchList>(RouteMatchList.empty);
}

if (state is! RouteInformationState) {
// This is a result of browser backward/forward button or state
// restoration. In this case, the route match list is already stored in
// the state.
final Object infoState = routeInformation.state!;
// Process legacy state if necessary.
if (infoState is! RouteInformationState) {
final RouteMatchList matchList =
_routeMatchListCodec.decode(state as Map<Object?, Object?>);
return debugParserFuture = _redirect(context, matchList)
.then<RouteMatchList>((RouteMatchList value) {
_routeMatchListCodec.decode(infoState as Map<Object?, Object?>);
return debugParserFuture =
_redirect(context, matchList).then((RouteMatchList value) {
if (value.isError && onParserException != null) {
// TODO(chunhtai): Figure out what to return if context is invalid.
// ignore: use_build_context_synchronously
return onParserException!(context, value);
}
_lastMatchList = value;
return value;
});
}

return _onEnterHandler.handleTopOnEnter(
context: context,
routeInformation: routeInformation,
infoState: infoState,
onCanEnter: () => _navigate(routeInformation, context, infoState),
onCanNotEnter: () {
// If navigation is blocked, return the last successful match or a fallback.
if (_lastMatchList != null) {
return SynchronousFuture<RouteMatchList>(_lastMatchList!);
} else {
final Uri defaultUri = Uri.parse(_initialLocation ?? '/');
final RouteMatchList fallbackMatches = configuration.findMatch(
defaultUri,
extra: infoState.extra,
);
_lastMatchList = fallbackMatches;
return SynchronousFuture<RouteMatchList>(fallbackMatches);
}
},
);
}

/// Normalizes the URI, finds matching routes, processes redirects, and updates
/// the route match list based on the navigation type.
Future<RouteMatchList> _navigate(
RouteInformation routeInformation,
BuildContext context,
RouteInformationState<dynamic> infoState,
) {
// Normalize the URI: ensure it has a valid path and remove trailing slashes.
Uri uri = routeInformation.uri;
if (uri.hasEmptyPath) {
uri = uri.replace(path: '/');
} else if (uri.path.length > 1 && uri.path.endsWith('/')) {
// Remove trailing `/`.
uri = uri.replace(path: uri.path.substring(0, uri.path.length - 1));
}

// Find initial route matches.
final RouteMatchList initialMatches = configuration.findMatch(
uri,
extra: state.extra,
extra: infoState.extra,
);
if (initialMatches.isError) {
log('No initial matches: ${routeInformation.uri.path}');
}

return debugParserFuture = _redirect(
context,
initialMatches,
).then<RouteMatchList>((RouteMatchList matchList) {
// Process any defined redirects.
return debugParserFuture =
_redirect(context, initialMatches).then((RouteMatchList matchList) {
if (matchList.isError && onParserException != null) {
// TODO(chunhtai): Figure out what to return if context is invalid.
// ignore: use_build_context_synchronously
return onParserException!(context, matchList);
}

// Validate that redirect-only routes actually perform a redirection.
assert(() {
if (matchList.isNotEmpty) {
assert(!matchList.last.route.redirectOnly,
'A redirect-only route must redirect to location different from itself.\n The offending route: ${matchList.last.route}');
assert(
!matchList.last.route.redirectOnly,
'Redirect-only route must redirect to a new location.\n'
'Offending route: ${matchList.last.route}',
);
}
return true;
}());
return _updateRouteMatchList(

// Update the route match list based on the navigation type.
final RouteMatchList updated = _updateRouteMatchList(
matchList,
baseRouteMatchList: state.baseRouteMatchList,
completer: state.completer,
type: state.type,
baseRouteMatchList: infoState.baseRouteMatchList,
completer: infoState.completer,
type: infoState.type,
);

// Cache the successful match list.
_lastMatchList = updated;
return updated;
});
}

@override
Future<RouteMatchList> parseRouteInformation(
RouteInformation routeInformation) {
// Not used in go_router; instruct users to use parseRouteInformationWithDependencies.
throw UnimplementedError(
'use parseRouteInformationWithDependencies instead');
'Use parseRouteInformationWithDependencies instead');
}

/// for use by the Router architecture as part of the RouteInformationParser
@override
RouteInformation? restoreRouteInformation(RouteMatchList configuration) {
if (configuration.isEmpty) {
@@ -139,15 +190,14 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
(configuration.matches.last is ImperativeRouteMatch ||
configuration.matches.last is ShellRouteMatch)) {
RouteMatchBase route = configuration.matches.last;

// Drill down to find the appropriate ImperativeRouteMatch.
while (route is! ImperativeRouteMatch) {
if (route is ShellRouteMatch && route.matches.isNotEmpty) {
route = route.matches.last;
} else {
break;
}
}

if (route case final ImperativeRouteMatch safeRoute) {
location = safeRoute.matches.uri.toString();
}
@@ -158,16 +208,21 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
);
}

/// Calls [configuration.redirect] and wraps the result in a synchronous future if needed.
Future<RouteMatchList> _redirect(
BuildContext context, RouteMatchList routeMatch) {
final FutureOr<RouteMatchList> redirectedFuture = configuration
.redirect(context, routeMatch, redirectHistory: <RouteMatchList>[]);
if (redirectedFuture is RouteMatchList) {
return SynchronousFuture<RouteMatchList>(redirectedFuture);
BuildContext context, RouteMatchList matchList) {
final FutureOr<RouteMatchList> result = configuration.redirect(
context,
matchList,
redirectHistory: <RouteMatchList>[],
);
if (result is RouteMatchList) {
return SynchronousFuture<RouteMatchList>(result);
}
return redirectedFuture;
return result;
}

/// Updates the route match list based on the navigation type (push, replace, etc.).
RouteMatchList _updateRouteMatchList(
RouteMatchList newMatchList, {
required RouteMatchList? baseRouteMatchList,
@@ -212,15 +267,21 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
case NavigatingType.go:
return newMatchList;
case NavigatingType.restore:
// Still need to consider redirection.
return baseRouteMatchList!.uri.toString() != newMatchList.uri.toString()
? newMatchList
: baseRouteMatchList;
// If the URIs differ, use the new one; otherwise, keep the old.
if (baseRouteMatchList!.uri.toString() != newMatchList.uri.toString()) {
return newMatchList;
} else {
return baseRouteMatchList;
}
}
}

/// Returns a unique [ValueKey<String>] for a new route.
ValueKey<String> _getUniqueValueKey() {
return ValueKey<String>(String.fromCharCodes(
List<int>.generate(32, (_) => _random.nextInt(33) + 89)));
return ValueKey<String>(
String.fromCharCodes(
List<int>.generate(32, (_) => _random.nextInt(33) + 89),
),
);
}
}
57 changes: 53 additions & 4 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'misc/inherited_router.dart';
import 'on_enter.dart';
import 'parser.dart';
import 'route.dart';
import 'state.dart';
@@ -41,6 +42,11 @@ class RoutingConfig {
/// The [routes] must not be empty.
const RoutingConfig({
required this.routes,
this.onEnter,
@Deprecated(
'Use onEnter instead. '
'This feature will be removed in a future release.',
)
this.redirect = _defaultRedirect,
this.redirectLimit = 5,
});
@@ -66,12 +72,45 @@ class RoutingConfig {
/// changes.
///
/// See [GoRouter].
@Deprecated(
'Use onEnter instead. '
'This feature will be removed in a future release.',
)
final GoRouterRedirect redirect;

/// The maximum number of redirection allowed.
///
/// See [GoRouter].
final int redirectLimit;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to handle infinite redirect, I thought we may still want to keep this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@chunhtai Is there any reason to have this public ? An arbitrary limit could be picked, eg: max 5 times on the same route and kept internal.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we let people pick their own limit, it was here since the v4 before flutter team took ownership. I find no reason to deprecate it, so it has been hanging around since.


/// A callback invoked for every incoming route before it is processed.
///
/// This callback allows you to control navigation by inspecting the incoming
/// route and conditionally preventing the navigation. If the callback returns
/// `true`, the GoRouter proceeds with the regular navigation and redirection
/// logic. If the callback returns `false`, the navigation is canceled.
///
/// When a deep link opens the app and `onEnter` returns `false`, GoRouter
/// will automatically redirect to the initial route or '/'.
///
/// Example:
/// ```dart
/// final GoRouter router = GoRouter(
/// routes: [...],
/// onEnter: (BuildContext context, Uri uri) {
/// if (uri.path == '/login' && isUserLoggedIn()) {
/// return false; // Prevent navigation to /login
/// }
/// if (uri.path == '/referral') {
/// // Save the referral code and prevent navigation
/// saveReferralCode(uri.queryParameters['code']);
/// return false;
/// }
/// return true; // Allow navigation
/// },
/// );
/// ```
final OnEnter? onEnter;
}

/// The route configuration for the app.
@@ -122,13 +161,18 @@ class GoRouter implements RouterConfig<RouteMatchList> {
/// The `routes` must not be null and must contain an [GoRouter] to match `/`.
factory GoRouter({
required List<RouteBase> routes,
OnEnter? onEnter,
Codec<Object?, Object?>? extraCodec,
GoExceptionHandler? onException,
GoRouterPageBuilder? errorPageBuilder,
GoRouterWidgetBuilder? errorBuilder,
@Deprecated(
'Use onEnter instead. '
'This feature will be removed in a future release.',
)
GoRouterRedirect? redirect,
Listenable? refreshListenable,
int redirectLimit = 5,
Listenable? refreshListenable,
bool routerNeglect = false,
String? initialLocation,
bool overridePlatformDefaultLocation = false,
@@ -142,9 +186,11 @@ class GoRouter implements RouterConfig<RouteMatchList> {
return GoRouter.routingConfig(
routingConfig: _ConstantRoutingConfig(
RoutingConfig(
routes: routes,
redirect: redirect ?? RoutingConfig._defaultRedirect,
redirectLimit: redirectLimit),
routes: routes,
redirect: redirect ?? RoutingConfig._defaultRedirect,
onEnter: onEnter,
redirectLimit: redirectLimit,
),
),
extraCodec: extraCodec,
onException: onException,
@@ -224,6 +270,8 @@ class GoRouter implements RouterConfig<RouteMatchList> {
routeInformationParser = GoRouteInformationParser(
onParserException: parserExceptionHandler,
configuration: configuration,
initialLocation: initialLocation,
router: this,
);

routeInformationProvider = GoRouteInformationProvider(
@@ -574,6 +622,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
/// A routing config that is never going to change.
class _ConstantRoutingConfig extends ValueListenable<RoutingConfig> {
const _ConstantRoutingConfig(this.value);

@override
void addListener(VoidCallback listener) {
// Intentionally empty because listener will never be called.
4 changes: 2 additions & 2 deletions packages/go_router/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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: 16.0.0
version: 16.1.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

@@ -28,4 +28,4 @@ dev_dependencies:
topics:
- deep-linking
- go-router
- navigation
- navigation
940 changes: 940 additions & 0 deletions packages/go_router/test/on_enter_test.dart

Large diffs are not rendered by default.