Skip to content

[go_router] feat: access GoRouter.of from redirect methods #9706

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 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions packages/go_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 16.0.1

- Fixes `GoRouter.of(context)` access inside redirect callbacks by providing router access through Zone-based context tracking.
- Adds support for using context extension methods (e.g., `context.namedLocation()`, `context.go()`) within redirect callbacks.

## 16.0.0

- **BREAKING CHANGE**
Expand Down
122 changes: 102 additions & 20 deletions packages/go_router/lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:flutter/widgets.dart';

import 'logging.dart';
import 'match.dart';
import 'misc/constants.dart';
import 'misc/errors.dart';
import 'path_utils.dart';
import 'route.dart';
Expand All @@ -29,6 +30,7 @@ class RouteConfiguration {
this._routingConfig, {
required this.navigatorKey,
this.extraCodec,
this.router,
}) {
_onRoutingTableChanged();
_routingConfig.addListener(_onRoutingTableChanged);
Expand Down Expand Up @@ -248,6 +250,11 @@ class RouteConfiguration {
/// example.
final Codec<Object?, Object?>? extraCodec;

/// The GoRouter instance that owns this configuration.
///
/// This is used to provide access to the router during redirects.
final GoRouter? router;

final Map<String, _NamedPath> _nameToPath = <String, _NamedPath>{};

/// Looks up the url location by a [GoRoute]'s name.
Expand Down Expand Up @@ -404,22 +411,40 @@ class RouteConfiguration {
}
return true;
});
final FutureOr<String?> routeLevelRedirectResult =
_getRouteLevelRedirect(context, prevMatchList, routeMatches, 0);

if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
try {
final FutureOr<String?> routeLevelRedirectResult =
_getRouteLevelRedirect(context, prevMatchList, routeMatches, 0);

if (routeLevelRedirectResult is String?) {
return processRouteLevelRedirect(routeLevelRedirectResult);
}
return routeLevelRedirectResult
.then<RouteMatchList>(processRouteLevelRedirect)
.catchError((Object error) {
final GoException goException = error is GoException
? error
: GoException('Exception during route redirect: $error');
return _errorRouteMatchList(prevMatchList.uri, goException,
extra: prevMatchList.extra);
});
} catch (exception) {
final GoException goException = exception is GoException
? exception
: GoException('Exception during route redirect: $exception');
return _errorRouteMatchList(prevMatchList.uri, goException,
extra: prevMatchList.extra);
}
return routeLevelRedirectResult
.then<RouteMatchList>(processRouteLevelRedirect);
}

redirectHistory.add(prevMatchList);
// Check for top-level redirect
final FutureOr<String?> topRedirectResult = _routingConfig.value.redirect(
context,
buildTopLevelGoRouterState(prevMatchList),
);
final FutureOr<String?> topRedirectResult = _runInRouterZone(() {
return _routingConfig.value.redirect(
context,
buildTopLevelGoRouterState(prevMatchList),
);
});

if (topRedirectResult is String?) {
return processTopLevelRedirect(topRedirectResult);
Expand Down Expand Up @@ -448,14 +473,34 @@ class RouteConfiguration {
_getRouteLevelRedirect(
context, matchList, routeMatches, currentCheckIndex + 1);
final RouteBase route = match.route;
final FutureOr<String?> routeRedirectResult = route.redirect!.call(
context,
match.buildState(this, matchList),
);
if (routeRedirectResult is String?) {
return processRouteRedirect(routeRedirectResult);
try {
final FutureOr<String?> routeRedirectResult = _runInRouterZone(() {
return route.redirect!.call(
context,
match.buildState(this, matchList),
);
});
if (routeRedirectResult is String?) {
return processRouteRedirect(routeRedirectResult);
}
return routeRedirectResult
.then<String?>(processRouteRedirect)
.catchError((Object error) {
// Convert any exception during async route redirect to a GoException
final GoException goException = error is GoException
? error
: GoException('Exception during route redirect: $error');
// Throw the GoException to be caught by the redirect handling chain
throw goException;
});
} catch (exception) {
// Convert any exception during route redirect to a GoException
final GoException goException = exception is GoException
? exception
: GoException('Exception during route redirect: $exception');
// Throw the GoException to be caught by the redirect handling chain
throw goException;
}
return routeRedirectResult.then<String?>(processRouteRedirect);
}

RouteMatchList _getNewMatches(
Expand All @@ -467,9 +512,12 @@ class RouteConfiguration {
final RouteMatchList newMatch = findMatch(Uri.parse(newLocation));
_addRedirect(redirectHistory, newMatch, previousLocation);
return newMatch;
} on GoException catch (e) {
log('Redirection exception: ${e.message}');
return _errorRouteMatchList(previousLocation, e);
} catch (exception) {
final GoException goException = exception is GoException
? exception
: GoException('Exception during redirect: $exception');
log('Redirection exception: ${goException.message}');
return _errorRouteMatchList(previousLocation, goException);
}
}

Expand Down Expand Up @@ -508,6 +556,40 @@ class RouteConfiguration {
.join(' => ');
}

/// Runs the given function in a Zone with the router context for redirects.
T _runInRouterZone<T>(T Function() callback) {
if (router == null) {
return callback();
}

T? result;
bool errorOccurred = false;

runZonedGuarded<void>(
() {
result = callback();
},
(Object error, StackTrace stack) {
errorOccurred = true;
// Convert any exception during redirect to a GoException and rethrow
final GoException goException = error is GoException
? error
: GoException('Exception during redirect: $error');
throw goException;
},
zoneValues: <Object?, Object?>{
currentRouterKey: router,
},
);

if (errorOccurred) {
// This should not be reached since we rethrow in the error handler
throw GoException('Unexpected error in router zone');
}

return result as T;
}

/// Get the location for the provided route.
///
/// Builds the absolute path for the route, by concatenating the paths of the
Expand Down
5 changes: 5 additions & 0 deletions packages/go_router/lib/src/misc/constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import 'package:meta/meta.dart';

/// Symbol used as a Zone key to track the current GoRouter during redirects.
@internal
const Symbol currentRouterKey = #goRouterRedirectContext;
2 changes: 0 additions & 2 deletions packages/go_router/lib/src/misc/extensions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import '../router.dart';
/// context.go('/');
extension GoRouterHelper on BuildContext {
/// Get a location from route name and parameters.
///
/// This method can't be called during redirects.
String namedLocation(
String name, {
Map<String, String> pathParameters = const <String, String>{},
Expand Down
39 changes: 34 additions & 5 deletions packages/go_router/lib/src/parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'configuration.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'misc/errors.dart';
import 'router.dart';

/// The function signature of [GoRouteInformationParser.onParserException].
Expand Down Expand Up @@ -160,12 +161,40 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {

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);
try {
final FutureOr<RouteMatchList> redirectedFuture = configuration
.redirect(context, routeMatch, redirectHistory: <RouteMatchList>[]);
if (redirectedFuture is RouteMatchList) {
return SynchronousFuture<RouteMatchList>(redirectedFuture);
}
return redirectedFuture.catchError((Object error) {
// Convert any exception during redirect to a GoException
final GoException goException = error is GoException
? error
: GoException('Exception during redirect: $error');
// Return an error match list instead of throwing
return RouteMatchList(
matches: const <RouteMatch>[],
extra: routeMatch.extra,
error: goException,
uri: routeMatch.uri,
pathParameters: const <String, String>{},
);
});
} catch (exception) {
// Convert any exception during redirect to a GoException
final GoException goException = exception is GoException
? exception
: GoException('Exception during redirect: $exception');
// Return an error match list instead of throwing
return SynchronousFuture<RouteMatchList>(RouteMatchList(
matches: const <RouteMatch>[],
extra: routeMatch.extra,
error: goException,
uri: routeMatch.uri,
pathParameters: const <String, String>{},
));
}
return redirectedFuture;
}

RouteMatchList _updateRouteMatchList(
Expand Down
22 changes: 14 additions & 8 deletions packages/go_router/lib/src/router.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'delegate.dart';
import 'information_provider.dart';
import 'logging.dart';
import 'match.dart';
import 'misc/constants.dart';
import 'misc/inherited_router.dart';
import 'parser.dart';
import 'route.dart';
Expand Down Expand Up @@ -206,6 +207,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
_routingConfig,
navigatorKey: navigatorKey,
extraCodec: extraCodec,
router: this,
);

final ParserExceptionHandler? parserExceptionHandler;
Expand Down Expand Up @@ -518,22 +520,26 @@ class GoRouter implements RouterConfig<RouteMatchList> {
}

/// Find the current GoRouter in the widget tree.
///
/// This method throws when it is called during redirects.
static GoRouter of(BuildContext context) {
final GoRouter? inherited = maybeOf(context);
assert(inherited != null, 'No GoRouter found in context');
return inherited!;
final GoRouter? router = maybeOf(context);
if (router == null) {
throw FlutterError('No GoRouter found in context');
}
return router;
}

/// The current GoRouter in the widget tree, if any.
///
/// This method returns null when it is called during redirects.
static GoRouter? maybeOf(BuildContext context) {
final InheritedGoRouter? inherited = context
.getElementForInheritedWidgetOfExactType<InheritedGoRouter>()
?.widget as InheritedGoRouter?;
return inherited?.goRouter;

if (inherited != null) {
return inherited.goRouter;
}

// Check if we're in a redirect context
return Zone.current[currentRouterKey] as GoRouter?;
}

/// Disposes resource created by this object.
Expand Down
2 changes: 1 addition & 1 deletion 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.0.1
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

Expand Down
Loading