-
Notifications
You must be signed in to change notification settings - Fork 3.4k
[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
base: main
Are you sure you want to change the base?
Changes from all commits
6be9a5d
171b639
f52a269
3bbd241
6a60006
d1e1fc2
516db13
e1f10b1
7a847b8
b08d804
1e25466
aec8e47
2bdc147
1bd3c18
61729b2
8334a64
f28337e
4092405
c1c09d0
d9e6ea6
07c15f0
3fbe011
67df52a
eef39b1
359eb0e
cc57519
d4f2416
56f2dbe
c458982
921dcb3
757f5a1
b5e1e9e
86c506b
3c4a85f
9d52c0d
97c5ed8
0323a45
4a9e6ff
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
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 { | ||
/// 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>{}, | ||
); | ||
} | ||
} |
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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. | ||
|
Large diffs are not rendered by default.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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!
I do not get that, can u clarify what u want here.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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()
orOnEnterResult.block()
? okay good one, @chunhtai what do you think ?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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