Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
61 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
3b2df49
[go_router] Make OnEnterHandler private and ensure onEnter priority o…
omar-hanafy Aug 2, 2025
1dd95d8
Merge branch 'main' into main
omar-hanafy Aug 2, 2025
ade3f54
Merge branch 'main' into main
omar-hanafy Aug 19, 2025
ed43b0b
Merge branch 'main' into main
omar-hanafy Aug 19, 2025
d0d5e6d
[go_router] added allow/block factories for the OnEnterResult
omar-hanafy Aug 19, 2025
a484965
[go_router] ran dart format
omar-hanafy Aug 19, 2025
10404f8
[go_router] returned back missing docs in GoRouterRedirect
omar-hanafy Aug 19, 2025
2b3d0be
[go_router] Fix license headers to match repository standards
omar-hanafy Aug 19, 2025
b554e6c
[go_router] sealed `onEnter` (Allow/Block.then) + compose legacy redi…
omar-hanafy Aug 20, 2025
99ab3c3
[go_router] Fix license headers to match repository standards
omar-hanafy Aug 20, 2025
265f26f
[go_router] Refactor parser and on_enter for clarity and type safety
omar-hanafy Aug 22, 2025
01f7ea6
[go_router] Remove unused initialLocation parameter from parser.
omar-hanafy Aug 22, 2025
22a29bd
[go_router] Ensure onEnter runs during state restoration, and unified…
omar-hanafy Aug 27, 2025
887c528
Merge branch 'main' into main
omar-hanafy Aug 28, 2025
37712e5
Merge branch 'main' into main
omar-hanafy Aug 30, 2025
ee55004
Merge branch 'main' into main
omar-hanafy Sep 21, 2025
9e37af9
[go_router] polish onEnter handling and docs
omar-hanafy Sep 22, 2025
faa9d81
Merge branch 'main' into main
omar-hanafy Sep 25, 2025
20c4148
[go_router] Refine onEnter documentation and API adjustments
omar-hanafy Sep 25, 2025
2b4c348
[go_router] Improve context safety and docs in navigation logic
omar-hanafy Sep 27, 2025
d1bc684
[go_router] Improve documentation for legacy redirect method
omar-hanafy Oct 13, 2025
54afc72
Merge branch 'main' into main
omar-hanafy Oct 15, 2025
33996c6
Merge remote-tracking branch 'upstream/main'
omar-hanafy Oct 16, 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
Prev Previous commit
Next Next commit
improved redirection and async handling.
  • Loading branch information
omar-hanafy committed Feb 18, 2025
commit f28337e7aba94a3c25a4abafeec1a36f25405096
2 changes: 1 addition & 1 deletion packages/go_router/example/lib/top_level_on_enter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class App extends StatelessWidget {
GoRouterState currentState,
GoRouterState nextState,
GoRouter goRouter,
) {
) async {
// Track analytics for deep links
if (nextState.uri.hasQuery || nextState.uri.hasFragment) {
_handleDeepLinkTracking(nextState.uri);
Expand Down
3 changes: 2 additions & 1 deletion packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ typedef GoRouterRedirect = FutureOr<String?> Function(
);

/// The signature of the onEnter callback.
typedef OnEnter = FutureOr<bool> Function(
typedef OnEnter = Future<bool> Function(
BuildContext context,
GoRouterState currentState,
GoRouterState nextState,
Expand Down Expand Up @@ -227,6 +227,7 @@ class RouteConfiguration {
extra: matchList.extra,
pageKey: const ValueKey<String>('topLevel'),
topRoute: matchList.lastOrNull?.route,
error: matchList.error,
);
}

Expand Down
237 changes: 124 additions & 113 deletions packages/go_router/lib/src/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -81,117 +81,64 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {

final Random _random = Random();

// Processes an onEnter navigation attempt. Returns an updated RouteMatchList.
// This is where the onEnter navigation logic happens.
// 1. Setting the Stage:
// We figure out the current and next states using the matchList and any previous successful match.
// 2. Calling onEnter:
// We call topOnEnter. It decides if navigation can happen. If yes, we update the match and return it.
// 3. The Safety Net (Last Successful Match):
// If navigation is blocked and we have a previous successful match, we go back to that.
// This provides a safe fallback (e.g., /) to prevent loops.
// 4. Loop Check:
// If there's no previous match, we check for loops. If the current URI is in the
// history, we're in a loop. Throw a GoException.
// 5. Redirection Limit:
// We check we haven't redirected too many times.
// 6. The Fallback (Initial Location):
// If not looping, and not over the redirect limit, go back to the start (initial location,
// usually /). We don't recurse. This treats places like / as final destinations,
// not part of a loop.
// This method avoids infinite loops but ensures we end up somewhere valid. Handling fallbacks
// like / prevents false loop detections and unnecessary recursion. It's about smooth,
// reliable navigation.
Future<RouteMatchList> _processOnEnter(
/// Tracks the URIs of onEnter redirections.
final List<Uri> _onEnterRedirectionHistory = <Uri>[];

/// Checks if the top-level onEnter callback allows navigation.
/// Returns true if allowed; otherwise, false.
/// If onEnter is null, navigation is always allowed.
Future<bool> _handleTopOnEnter(
BuildContext context,
RouteMatchList matchList,
List<RouteMatchList> onEnterHistory,
) async {
// Build states for onEnter
RouteInformation routeInformation,
RouteInformationState<dynamic> infoState,
) {
final OnEnter? topOnEnter = configuration.topOnEnter;
if (topOnEnter == null) {
return SynchronousFuture<bool>(true);
}

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

// Construct navigation states.
final GoRouterState nextState =
configuration.buildTopLevelGoRouterState(matchList);
configuration.buildTopLevelGoRouterState(incomingMatches);
final GoRouterState currentState = _lastMatchList != null
? configuration.buildTopLevelGoRouterState(_lastMatchList!)
: nextState;

// Invoke the onEnter callback
final bool canEnter = await configuration.topOnEnter!(
// Execute the onEnter callback.
final FutureOr<bool> result = topOnEnter(
context,
currentState,
nextState,
_router,
);

// If navigation is allowed, update and return immediately
if (canEnter) {
_lastMatchList = matchList;
return _updateRouteMatchList(
matchList,
baseRouteMatchList: matchList,
completer: null,
type: NavigatingType.go,
);
}

// If we have a last successful match, use it as fallback WITHOUT recursion
if (_lastMatchList != null) {
return _updateRouteMatchList(
_lastMatchList!,
baseRouteMatchList: matchList,
completer: null,
type: NavigatingType.go,
);
}

// Check for loops
if (onEnterHistory.length > 1 &&
onEnterHistory.any((RouteMatchList m) => m.uri == matchList.uri)) {
throw GoException(
'onEnter redirect loop detected: ${onEnterHistory.map((RouteMatchList m) => m.uri).join(' => ')} => ${matchList.uri}',
);
}

// Check redirect limit before continuing
if (onEnterHistory.length >= configuration.redirectLimit) {
throw GoException(
'Too many onEnter redirects: ${onEnterHistory.map((RouteMatchList m) => m.uri).join(' => ')}',
);
}

// Add current match to history
onEnterHistory.add(matchList);

// Try initial location as fallback WITHOUT recursion
final RouteMatchList fallbackMatches = configuration.findMatch(
Uri.parse(_initialLocation ?? '/'),
extra: matchList.extra,
GoRouter.maybeOf(context) ?? _router,
Copy link
Contributor

@cedvdb cedvdb Feb 18, 2025

Choose a reason for hiding this comment

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

Is there a reason why not use only _router directly here ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I am thinking that the router with the context might be slightly different actually did not chrck that though. If u think the router instance is enough lets remove that.

Copy link
Contributor

@cedvdb cedvdb Feb 18, 2025

Choose a reason for hiding this comment

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

Yes I believe it should be only _router.

);

return _updateRouteMatchList(
fallbackMatches,
baseRouteMatchList: matchList,
completer: null,
type: NavigatingType.go,
);
// Wrap immediate results in a SynchronousFuture.
return (result is bool)
? SynchronousFuture<bool>(result)
: Future<bool>.value(result);
}

/// Parses route information and handles navigation decisions based on various states and callbacks.
/// This is called by the [Router] when a new route needs to be processed, such as during deep linking,
/// browser navigation, or in-app navigation.
/// Parses route information and determines the navigation outcome.
/// Handles both legacy (non-RouteInformationState) and current route states.
@override
Future<RouteMatchList> parseRouteInformationWithDependencies(
RouteInformation routeInformation,
BuildContext context,
) {
// 1) Safety check
// Safety check
if (routeInformation.state == null) {
return SynchronousFuture<RouteMatchList>(RouteMatchList.empty);
}

final Object infoState = routeInformation.state!;

// 2) Handle restored navigation
if (infoState is! RouteInformationState) {
// Decode the legacy state and apply redirects.
final RouteMatchList matchList =
_routeMatchListCodec.decode(infoState as Map<Object?, Object?>);

Expand All @@ -205,67 +152,131 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
});
}

// 3) Normalize the URI first
// handle redirection limit
if (configuration.topOnEnter != null) {
// A redirection is being triggered via onEnter.
_onEnterRedirectionHistory.add(routeInformation.uri);
if (_onEnterRedirectionHistory.length > configuration.redirectLimit) {
final String formattedHistory =
_formatOnEnterRedirectionHistory(_onEnterRedirectionHistory);

final RouteMatchList errorMatchList = _errorRouteMatchList(
routeInformation.uri,
GoException('Too many onEnter calls detected: $formattedHistory'),
);

_onEnterRedirectionHistory.clear();
return SynchronousFuture<RouteMatchList>(
onParserException != null
? onParserException!(context, errorMatchList)
: errorMatchList,
);
}
}

// Use onEnter to decide if navigation should proceed.
final Future<bool> canEnterFuture = _handleTopOnEnter(
context,
routeInformation,
infoState,
);

return canEnterFuture.then(
(bool canEnter) {
_onEnterRedirectionHistory.clear();
if (!canEnter) {
// 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);
}
} else {
// Navigation allowed: clear redirection history.
return _navigate(routeInformation, context, infoState);
}
},
);
}

/// The match used when there is an error during 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>{},
);
}

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

/// 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('/')) {
uri = uri.replace(path: uri.path.substring(0, uri.path.length - 1));
}

// Find initial matches for the normalized URI
// Find initial route matches.
final RouteMatchList initialMatches = configuration.findMatch(
uri,
extra: infoState.extra,
);

// 4) Handle route interception via onEnter callback
if (configuration.topOnEnter != null) {
// Call _processOnEnter and immediately return its result
return _processOnEnter(
context,
initialMatches,
<RouteMatchList>[initialMatches], // Start history with initial match
);
}

// 5) If onEnter isn't used or throws, continue with redirect processing
if (initialMatches.isError) {
log('No initial matches: ${routeInformation.uri.path}');
}

// 6) Process any redirects defined in the route configuration
// Routes might need to redirect based on auth state or other conditions
// Process any defined redirects.
return debugParserFuture =
_redirect(context, initialMatches).then((RouteMatchList matchList) {
// Handle any errors during route matching/redirection
if (matchList.isError && onParserException != null) {
return onParserException!(context, matchList);
}

// 7) Development-time check for redirect-only routes
// Redirect-only routes must actually redirect somewhere else
// Ensure that redirect-only routes actually perform a redirection.
assert(() {
if (matchList.isNotEmpty) {
assert(
!matchList.last.route.redirectOnly,
'A redirect-only route must redirect to a different location.\n'
'Offending route: ${matchList.last.route}');
!matchList.last.route.redirectOnly,
'Redirect-only route must redirect to a new location.\n'
'Offending route: ${matchList.last.route}',
);
}
return true;
}());

// 8) Handle specific navigation types (push, replace, etc.)
// Different navigation actions need different route stack manipulations
// Update the route match list according to the navigation type (push, replace, etc.).
final RouteMatchList updated = _updateRouteMatchList(
matchList,
baseRouteMatchList: infoState.baseRouteMatchList,
completer: infoState.completer,
type: infoState.type,
);

// 9) Cache this successful route match for future reference
// We need this for comparison in onEnter and fallback in navigation failure
// Cache the successful match list.
_lastMatchList = updated;
return updated;
});
Expand Down
Loading