Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5cea2f7
feat: access GoRouter.of from redirect methods
tomassasovsky Jul 30, 2025
05b94b7
refactor: update documentation for navigatorKey and extraCodec in Rou…
tomassasovsky Jul 30, 2025
c93570b
refactor: replace _currentRouterKey with currentRouterKey constant fo…
tomassasovsky Jul 30, 2025
c94be6f
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Jul 30, 2025
858ea74
chore: release version 16.0.1 with fixes for redirect callbacks and c…
tomassasovsky Jul 31, 2025
d2b6d00
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Jul 31, 2025
d90201c
Merge branch 'feat/go-router-context-redirect' of github.com:tomassas…
tomassasovsky Jul 31, 2025
61bc8ff
refactor: update documentation for GoRouter context methods to clarif…
tomassasovsky Jul 31, 2025
6309387
feat: implement error handling in router zone for GoRouter
tomassasovsky Aug 1, 2025
fec0356
refactor: replace runZoned with runZonedGuarded for improved error ha…
tomassasovsky Aug 2, 2025
e756161
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Aug 2, 2025
2cdaa91
feat: enhance error handling in redirect callbacks and tests for GoRo…
tomassasovsky Aug 5, 2025
52d78a7
Merge branch 'feat/go-router-context-redirect' of github.com:tomassas…
tomassasovsky Aug 5, 2025
6221028
Merge branch 'main' of github.com:flutter/packages into feat/go-route…
tomassasovsky Sep 24, 2025
05c1cbc
chore: dart format
tomassasovsky Sep 25, 2025
7d8d0d5
chore: add copyright to constants.dart
tomassasovsky Sep 25, 2025
7beaf8e
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Sep 25, 2025
1619f40
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Sep 26, 2025
ddb6e9d
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Oct 7, 2025
6bd4af8
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Oct 9, 2025
f9b088f
Merge branch 'main' into feat/go-router-context-redirect
tomassasovsky Oct 9, 2025
eea054a
Merge branch 'main' into feat/go-router-context-redirect
chunhtai Oct 14, 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
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.2.5

- 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.2.4

- Fix Android Cold Start deep link with empty path losing scheme and authority.
Expand Down
132 changes: 111 additions & 21 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 @@ -265,6 +267,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 @@ -430,23 +437,50 @@ 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 @@ -479,14 +513,33 @@ class RouteConfiguration {
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 @@ -498,9 +551,13 @@ 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 @@ -536,6 +593,39 @@ 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
9 changes: 9 additions & 0 deletions packages/go_router/lib/src/misc/constants.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.

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
49 changes: 41 additions & 8 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 @@ -170,15 +171,47 @@ class GoRouteInformationParser extends RouteInformationParser<RouteMatchList> {
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
21 changes: 13 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 @@ -209,6 +210,7 @@ class GoRouter implements RouterConfig<RouteMatchList> {
_routingConfig,
navigatorKey: navigatorKey,
extraCodec: extraCodec,
router: this,
);

final ParserExceptionHandler? parserExceptionHandler;
Expand Down Expand Up @@ -537,24 +539,27 @@ 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.2.4
version: 16.2.5
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