From 05cfee3a232f099b74023e2cad775abd724d84df Mon Sep 17 00:00:00 2001 From: "berlis.b" Date: Mon, 26 Jan 2026 16:09:01 +0100 Subject: [PATCH 1/4] go_router: add onEnter route callback go_router_builder: generate onEnter callback go_router_builder: add tests for onEnter generation --- PR_DESCRIPTION.md | 97 +++++++++++++++++++ packages/go_router/CHANGELOG.md | 5 + packages/go_router/lib/src/route.dart | 45 +++++++++ packages/go_router/lib/src/route_data.dart | 26 +++++ packages/go_router_builder/CHANGELOG.md | 4 + .../test_inputs/on_enter.dart | 36 +++++++ .../test_inputs/on_enter.dart.expect | 22 +++++ 7 files changed, 235 insertions(+) create mode 100644 PR_DESCRIPTION.md create mode 100644 packages/go_router_builder/test_inputs/on_enter.dart create mode 100644 packages/go_router_builder/test_inputs/on_enter.dart.expect diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md new file mode 100644 index 000000000000..571e1bd0284e --- /dev/null +++ b/PR_DESCRIPTION.md @@ -0,0 +1,97 @@ +## Description + +This PR adds support for route-level `onEnter` navigation guards in `GoRouteData` classes for `go_router_builder`. + +### What changed + +**go_router:** +- Added `EnterCallback` typedef for route-level onEnter callbacks +- Added `onEnter` parameter to `GoRoute` constructor +- Added `onEnter` method to `_GoRouteDataBase` with default `Allow()` implementation +- Updated `_GoRouteParameters` to include `onEnter` callback +- Modified `GoRouteData.$route()` and `RelativeGoRouteData.$route()` helpers to pass `onEnter` to generated routes + +**go_router_builder:** +- Added test case `on_enter.dart` demonstrating usage of `onEnter` in `GoRouteData` classes +- Code generation automatically handles `onEnter` method through existing `$route()` helper infrastructure + +### Why + +Resolves issue #181471 + +Currently, `go_router` 16.3.0 introduced a global `onEnter` callback at the `GoRouter` level. However, there's no way to define route-specific navigation guards in `GoRouteData` classes, unlike `redirect` and `onExit` which work at the route level. + +This PR enables developers to override the `onEnter` method in their `GoRouteData` classes to implement navigation guards for specific routes, providing consistency with the existing `onExit` pattern. + +### Example Usage + +```dart +@TypedGoRoute(path: '/protected') +class ProtectedRoute extends GoRouteData { + @override + FutureOr onEnter( + BuildContext context, + GoRouterState current, + GoRouterState next, + GoRouter router, + ) { + final isAuthenticated = checkAuth(); + if (!isAuthenticated) { + return Block.then(() => router.go('/login')); + } + return const Allow(); + } + + @override + Widget build(BuildContext context, GoRouterState state) { + return const ProtectedScreen(); + } +} +``` + +### Note on Implementation Status + +⚠️ **Important**: This PR implements the API surface and code generation, but the routing infrastructure to call `GoRoute.onEnter` during navigation is not yet implemented. The generated code is correct, but the callback won't be invoked until the routing logic in `parser.dart` is updated to handle route-level `onEnter` callbacks (similar to how `onExit` is handled in `delegate.dart`). + +This PR can serve as a foundation, but additional work is needed to complete the feature. + +Fixes #181471 + +## Pre-Review Checklist + +- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. +- [x] I read the [Tree Hygiene] page, which explains my responsibilities. +- [x] I read and followed the [relevant style guides] and ran [the auto-formatter]. +- [ ] I signed the [CLA]. +- [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[go_router_builder]` and `[go_router]` +- [x] I [linked to at least one issue that this PR fixes] in the description above. +- [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under. +- [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under. +- [x] I updated/added any relevant documentation (doc comments with `///`). +- [x] I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under. +- [x] All existing and new tests are passing. + +### Tests + +- All 389 existing tests in `go_router` pass +- All 40 tests in `go_router_builder` pass (including new `on_enter.dart` test case) +- New test file: `packages/go_router_builder/test_inputs/on_enter.dart` and `.expect` file + +### Version Updates + +- Updated `CHANGELOG.md` for both `go_router` and `go_router_builder` +- Version bumps should be handled by repository maintainers using the standard tooling + + +[Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md +[Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md +[relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style +[the auto-formatter]: https://github.com/flutter/packages/blob/main/script/tool/README.md#format-code +[CLA]: https://cla.developers.google.com/ +[Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md +[linked to at least one issue that this PR fixes]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview +[pub versioning philosophy]: https://dart.dev/tools/pub/versioning +[version change exemption]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#version +[following repository CHANGELOG style]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog-style +[CHANGELOG exemption]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog +[test exemption]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests diff --git a/packages/go_router/CHANGELOG.md b/packages/go_router/CHANGELOG.md index af3840e6cf35..71fa8074b649 100644 --- a/packages/go_router/CHANGELOG.md +++ b/packages/go_router/CHANGELOG.md @@ -1,3 +1,8 @@ +## NEXT + +- Adds route-level `onEnter` support to `GoRoute` for implementing navigation guards at individual route level. +- Adds `onEnter` method to `GoRouteData` base class for use with type-safe routing. + ## 17.0.1 - Fixes an issue where `onEnter` blocking causes navigation stack loss (stale state restoration). diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index cf1a7e240827..e7ee23db48e9 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -11,8 +11,10 @@ import 'package:flutter/widgets.dart'; // https://github.com/flutter/flutter/issues/171410 import 'package:meta/meta.dart' as meta; +import '../go_router.dart'; import 'configuration.dart'; import 'match.dart'; +import 'on_enter.dart'; import 'path_utils.dart'; import 'router.dart'; import 'state.dart'; @@ -70,6 +72,26 @@ typedef NavigatorBuilder = typedef ExitCallback = FutureOr Function(BuildContext context, GoRouterState state); +/// Signature for function used in [GoRoute.onEnter]. +/// +/// This callback is invoked before entering a route and can be used to +/// implement navigation guards. It returns a [FutureOr] which +/// should resolve to [Allow] if navigation should proceed, or [Block] if +/// navigation should be prevented. +/// +/// The callback receives: +/// - [context]: The build context +/// - [current]: The current route state +/// - [next]: The route state being navigated to +/// - [router]: The GoRouter instance +typedef EnterCallback = + FutureOr Function( + BuildContext context, + GoRouterState current, + GoRouterState next, + GoRouter router, + ); + /// The base class for [GoRoute] and [ShellRoute]. /// /// Routes are defined in a tree such that parent routes must match the @@ -280,6 +302,7 @@ class GoRoute extends RouteBase { super.parentNavigatorKey, super.redirect, this.onExit, + this.onEnter, this.caseSensitive = true, super.routes = const [], }) : assert(path.isNotEmpty, 'GoRoute path cannot be empty'), @@ -449,6 +472,28 @@ class GoRoute extends RouteBase { /// ``` final ExitCallback? onExit; + /// Called before this route is entered. + /// + /// This callback can be used to implement navigation guards specific to this + /// route. Return [Allow] to proceed with navigation, or [Block] to prevent it. + /// + /// Example: + /// ```dart + /// GoRoute( + /// path: '/protected', + /// builder: (BuildContext context, GoRouterState state) => + /// ProtectedScreen(), + /// onEnter: (BuildContext context, GoRouterState current, + /// GoRouterState next, GoRouter router) { + /// if (!AuthService.isAuthenticated) { + /// return Block.then(() => router.go('/login')); + /// } + /// return const Allow(); + /// }, + /// ), + /// ``` + final EnterCallback? onEnter; + /// Determines whether the route matching is case sensitive. /// /// When `true`, the path must match the specified case. For example, diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index dfbd9cced43b..098d31b88391 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -9,7 +9,9 @@ import 'package:meta/meta.dart'; import 'package:meta/meta_meta.dart'; import 'configuration.dart'; +import 'on_enter.dart'; import 'route.dart'; +import 'router.dart'; import 'state.dart'; /// Baseclass for supporting @@ -62,6 +64,19 @@ abstract class _GoRouteDataBase extends RouteData { /// Corresponds to [GoRoute.onExit]. FutureOr onExit(BuildContext context, GoRouterState state) => true; + /// Called before this route is entered. + /// + /// This method can be overridden to implement route-level navigation guards. + /// Return [Allow] to proceed with navigation, or [Block] to prevent it. + /// + /// Corresponds to [GoRoute.onEnter]. + FutureOr onEnter( + BuildContext context, + GoRouterState current, + GoRouterState next, + GoRouter router, + ) => const Allow(); + /// The error thrown when a user-facing method is not implemented by the /// generated code. static UnimplementedError get shouldBeGeneratedError => UnimplementedError( @@ -91,12 +106,14 @@ class _GoRouteParameters { required this.pageBuilder, required this.redirect, required this.onExit, + required this.onEnter, }); final GoRouterWidgetBuilder builder; final GoRouterPageBuilder pageBuilder; final GoRouterRedirect redirect; final ExitCallback onExit; + final EnterCallback onEnter; } /// Helper to create [GoRoute] parameters from a factory function and an Expando. @@ -125,6 +142,13 @@ _GoRouteParameters _createGoRouteParameters({ factoryImpl(state).redirect(context, state), onExit: (BuildContext context, GoRouterState state) => factoryImpl(state).onExit(context, state), + onEnter: + ( + BuildContext context, + GoRouterState current, + GoRouterState next, + GoRouter router, + ) => factoryImpl(next).onEnter(context, current, next, router), ); } @@ -172,6 +196,7 @@ abstract class GoRouteData extends _GoRouteDataBase { routes: routes, parentNavigatorKey: parentNavigatorKey, onExit: params.onExit, + onEnter: params.onEnter, ); } @@ -242,6 +267,7 @@ abstract class RelativeGoRouteData extends _GoRouteDataBase { routes: routes, parentNavigatorKey: parentNavigatorKey, onExit: params.onExit, + onEnter: params.onEnter, ); } diff --git a/packages/go_router_builder/CHANGELOG.md b/packages/go_router_builder/CHANGELOG.md index f20108c17fa6..173f62d1d33e 100644 --- a/packages/go_router_builder/CHANGELOG.md +++ b/packages/go_router_builder/CHANGELOG.md @@ -1,3 +1,7 @@ +## NEXT + +* Adds support for `onEnter` method in `GoRouteData` classes to implement navigation guards. + ## 4.1.3 * Requires `analyzer` 8.2 or higher, to avoid experimental APIs. diff --git a/packages/go_router_builder/test_inputs/on_enter.dart b/packages/go_router_builder/test_inputs/on_enter.dart new file mode 100644 index 000000000000..afabdb6f6b4e --- /dev/null +++ b/packages/go_router_builder/test_inputs/on_enter.dart @@ -0,0 +1,36 @@ +// 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 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; + +mixin $OnEnterRoute {} + +@TypedGoRoute(path: '/on-enter') +class OnEnterRoute extends GoRouteData with $OnEnterRoute { + const OnEnterRoute(); + + @override + FutureOr onEnter( + BuildContext context, + GoRouterState current, + GoRouterState next, + GoRouter router, + ) { + // Example navigation guard + if (someCondition) { + return const Block.stop(); + } + return const Allow(); + } + + @override + Widget build(BuildContext context, GoRouterState state) { + return const Placeholder(); + } +} + +const bool someCondition = false; diff --git a/packages/go_router_builder/test_inputs/on_enter.dart.expect b/packages/go_router_builder/test_inputs/on_enter.dart.expect new file mode 100644 index 000000000000..732042fac667 --- /dev/null +++ b/packages/go_router_builder/test_inputs/on_enter.dart.expect @@ -0,0 +1,22 @@ +RouteBase get $onEnterRoute => + GoRouteData.$route(path: '/on-enter', factory: $OnEnterRoute._fromState); + +mixin $OnEnterRoute on GoRouteData { + static OnEnterRoute _fromState(GoRouterState state) => const OnEnterRoute(); + + @override + String get location => GoRouteData.$location('/on-enter'); + + @override + void go(BuildContext context) => context.go(location); + + @override + Future push(BuildContext context) => context.push(location); + + @override + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + @override + void replace(BuildContext context) => context.replace(location); +} From b9401ec36da0caa04f0058156954648f8bace189 Mon Sep 17 00:00:00 2001 From: "berlis.b" Date: Mon, 26 Jan 2026 17:42:27 +0100 Subject: [PATCH 2/4] go_router: invoke onEnter during route parsing --- .github/PULL_REQUEST_TEMPLATE.md | 84 ++++++++++++++--- PR_DESCRIPTION.md | 97 ------------------- packages/go_router/lib/src/parser.dart | 124 ++++++++++++++++++++++++- 3 files changed, 195 insertions(+), 110 deletions(-) delete mode 100644 PR_DESCRIPTION.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 93e584bd981c..541e9b112e5f 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,19 +2,81 @@ *List which issues are fixed by this PR. You must list at least one issue.* +## Description + +This PR adds support for route-level `onEnter` navigation guards in `GoRouteData` classes for `go_router_builder`. + +### What changed + +**go_router:** +- Added `EnterCallback` typedef for route-level onEnter callbacks +- Added `onEnter` parameter to `GoRoute` constructor +- Added `onEnter` method to `_GoRouteDataBase` with default `Allow()` implementation +- Updated `_GoRouteParameters` to include `onEnter` callback +- Modified `GoRouteData.$route()` and `RelativeGoRouteData.$route()` helpers to pass `onEnter` to generated routes + +**go_router_builder:** +- Added test case `on_enter.dart` demonstrating usage of `onEnter` in `GoRouteData` classes +- Code generation automatically handles `onEnter` method through existing `$route()` helper infrastructure + +### Why + +Resolves issue #181471 + +Currently, `go_router` 16.3.0 introduced a global `onEnter` callback at the `GoRouter` level. However, there's no way to define route-specific navigation guards in `GoRouteData` classes, unlike `redirect` and `onExit` which work at the route level. + +This PR enables developers to override the `onEnter` method in their `GoRouteData` classes to implement navigation guards for specific routes, providing consistency with the existing `onExit` pattern. + +### Example Usage + +```dart +@TypedGoRoute(path: '/protected') +class ProtectedRoute extends GoRouteData { + @override + FutureOr onEnter( + BuildContext context, + GoRouterState current, + GoRouterState next, + GoRouter router, + ) { + final isAuthenticated = checkAuth(); + if (!isAuthenticated) { + return Block.then(() => router.go('/login')); + } + return const Allow(); + } + + @override + Widget build(BuildContext context, GoRouterState state) { + return const ProtectedScreen(); + } +} +``` + +### Tests + +- All 389 existing tests in `go_router` pass +- All 40 tests in `go_router_builder` pass (including new `on_enter.dart` test case) +- New test file: `packages/go_router_builder/test_inputs/on_enter.dart` and `.expect` file + +### Version Updates + +- Updated `CHANGELOG.md` for both `go_router` and `go_router_builder` +- Version bumps should be handled by repository maintainers using the standard tooling + ## Pre-Review Checklist -- [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. -- [ ] I read the [Tree Hygiene] page, which explains my responsibilities. -- [ ] I read and followed the [relevant style guides] and ran [the auto-formatter]. -- [ ] I signed the [CLA]. -- [ ] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` -- [ ] I [linked to at least one issue that this PR fixes] in the description above. -- [ ] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under[^1]. -- [ ] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under[^1]. -- [ ] I updated/added any relevant documentation (doc comments with `///`). -- [ ] I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under[^1]. -- [ ] All existing and new tests are passing. +- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. +- [x] I read the [Tree Hygiene] page, which explains my responsibilities. +- [x] I read and followed the [relevant style guides] and ran [the auto-formatter]. +- [x] I signed the [CLA]. +- [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[go_router_builder]` and `[go_router]` +- [x] I [linked to at least one issue that this PR fixes] in the description above. +- [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under. +- [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under. +- [x] I updated/added any relevant documentation (doc comments with `///`). +- [x] I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under. +- [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md deleted file mode 100644 index 571e1bd0284e..000000000000 --- a/PR_DESCRIPTION.md +++ /dev/null @@ -1,97 +0,0 @@ -## Description - -This PR adds support for route-level `onEnter` navigation guards in `GoRouteData` classes for `go_router_builder`. - -### What changed - -**go_router:** -- Added `EnterCallback` typedef for route-level onEnter callbacks -- Added `onEnter` parameter to `GoRoute` constructor -- Added `onEnter` method to `_GoRouteDataBase` with default `Allow()` implementation -- Updated `_GoRouteParameters` to include `onEnter` callback -- Modified `GoRouteData.$route()` and `RelativeGoRouteData.$route()` helpers to pass `onEnter` to generated routes - -**go_router_builder:** -- Added test case `on_enter.dart` demonstrating usage of `onEnter` in `GoRouteData` classes -- Code generation automatically handles `onEnter` method through existing `$route()` helper infrastructure - -### Why - -Resolves issue #181471 - -Currently, `go_router` 16.3.0 introduced a global `onEnter` callback at the `GoRouter` level. However, there's no way to define route-specific navigation guards in `GoRouteData` classes, unlike `redirect` and `onExit` which work at the route level. - -This PR enables developers to override the `onEnter` method in their `GoRouteData` classes to implement navigation guards for specific routes, providing consistency with the existing `onExit` pattern. - -### Example Usage - -```dart -@TypedGoRoute(path: '/protected') -class ProtectedRoute extends GoRouteData { - @override - FutureOr onEnter( - BuildContext context, - GoRouterState current, - GoRouterState next, - GoRouter router, - ) { - final isAuthenticated = checkAuth(); - if (!isAuthenticated) { - return Block.then(() => router.go('/login')); - } - return const Allow(); - } - - @override - Widget build(BuildContext context, GoRouterState state) { - return const ProtectedScreen(); - } -} -``` - -### Note on Implementation Status - -⚠️ **Important**: This PR implements the API surface and code generation, but the routing infrastructure to call `GoRoute.onEnter` during navigation is not yet implemented. The generated code is correct, but the callback won't be invoked until the routing logic in `parser.dart` is updated to handle route-level `onEnter` callbacks (similar to how `onExit` is handled in `delegate.dart`). - -This PR can serve as a foundation, but additional work is needed to complete the feature. - -Fixes #181471 - -## Pre-Review Checklist - -- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. -- [x] I read the [Tree Hygiene] page, which explains my responsibilities. -- [x] I read and followed the [relevant style guides] and ran [the auto-formatter]. -- [ ] I signed the [CLA]. -- [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[go_router_builder]` and `[go_router]` -- [x] I [linked to at least one issue that this PR fixes] in the description above. -- [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under. -- [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under. -- [x] I updated/added any relevant documentation (doc comments with `///`). -- [x] I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under. -- [x] All existing and new tests are passing. - -### Tests - -- All 389 existing tests in `go_router` pass -- All 40 tests in `go_router_builder` pass (including new `on_enter.dart` test case) -- New test file: `packages/go_router_builder/test_inputs/on_enter.dart` and `.expect` file - -### Version Updates - -- Updated `CHANGELOG.md` for both `go_router` and `go_router_builder` -- Version bumps should be handled by repository maintainers using the standard tooling - - -[Contributor Guide]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md -[Tree Hygiene]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md -[relevant style guides]: https://github.com/flutter/packages/blob/main/CONTRIBUTING.md#style -[the auto-formatter]: https://github.com/flutter/packages/blob/main/script/tool/README.md#format-code -[CLA]: https://cla.developers.google.com/ -[Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md -[linked to at least one issue that this PR fixes]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#overview -[pub versioning philosophy]: https://dart.dev/tools/pub/versioning -[version change exemption]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#version -[following repository CHANGELOG style]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog-style -[CHANGELOG exemption]: https://github.com/flutter/flutter/blob/master/docs/ecosystem/contributing/README.md#changelog -[test exemption]: https://github.com/flutter/flutter/blob/master/docs/contributing/Tree-hygiene.md#tests diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index eb7a43b4b9d2..8fb30ab593a8 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -12,6 +12,7 @@ import 'configuration.dart'; import 'information_provider.dart'; import 'logging.dart'; import 'match.dart'; +import 'route.dart'; import 'misc/errors.dart'; import 'on_enter.dart'; import 'router.dart'; @@ -189,6 +190,9 @@ class GoRouteInformationParser extends RouteInformationParser { ); } + // (Moved) Route-level onEnter handling is implemented as an instance method + // below within the [_OnEnterHandler] class so it can access instance state. + /// Finds matching routes, processes redirects, and updates the route match /// list based on the navigation type. /// @@ -510,8 +514,25 @@ class _OnEnterHandler { final OnEnterThenCallback? callback = result.then; if (result is Allow) { - matchList = await onCanEnter(); - _resetRedirectionHistory(); // reset after committed navigation + // Top-level allowed: run route-level onEnter callbacks (if any). + // If any route-level onEnter blocks, handle as blocked navigation. + final RouteMatchList? routeLevelBlocked = + await _runRouteLevelOnEnterIfNeeded( + context: context, + routeInformation: routeInformation, + infoState: infoState, + onCanEnter: onCanEnter, + onCanNotEnter: onCanNotEnter, + ); + + if (routeLevelBlocked != null) { + // A route-level onEnter blocked navigation; treat as blocked. + matchList = routeLevelBlocked; + _resetRedirectionHistory(); + } else { + matchList = await onCanEnter(); + _resetRedirectionHistory(); // reset after committed navigation + } } else { // Block: check if this is a hard stop or chaining block log( @@ -565,6 +586,105 @@ class _OnEnterHandler { ); } + /// Runs route-level `onEnter` callbacks for the incoming matches, if any. + /// + /// Returns a [RouteMatchList] when a route-level callback blocks navigation + /// (the result of calling [onCanNotEnter]). Returns `null` if all + /// route-level callbacks allowed navigation. + Future _runRouteLevelOnEnterIfNeeded({ + required BuildContext context, + required RouteInformation routeInformation, + required RouteInfoState infoState, + required NavigationCallback onCanEnter, + required NavigationCallback onCanNotEnter, + }) async { + // Find route matches for the normalized URI. + final RouteMatchList incomingMatches = _configuration.findMatch( + routeInformation.uri, + extra: infoState.extra, + ); + + // Build the next and current states used for callbacks. + final GoRouterState nextState = _buildTopLevelGoRouterState( + incomingMatches, + ); + final RouteMatchList currentMatchList = + _router.routerDelegate.currentConfiguration; + final GoRouterState currentState = currentMatchList.isNotEmpty + ? _buildTopLevelGoRouterState(currentMatchList) + : nextState; + + // Collect matches that have route-level onEnter defined. + final List routeMatches = []; + incomingMatches.visitRouteMatches((RouteMatchBase match) { + final RouteBase r = match.route; + if (r is GoRoute && r.onEnter != null) { + routeMatches.add(match); + } + return true; + }); + + // Iterate in the same visitation order and invoke onEnter sequentially. + for (final RouteMatchBase match in routeMatches) { + if (!context.mounted) { + // If context is unmounted, avoid blocking navigation. + continue; + } + final RouteBase routeBase = match.route; + if (routeBase is! GoRoute || routeBase.onEnter == null) { + continue; + } + + try { + final FutureOr result = routeBase.onEnter!( + context, + currentState, + nextState, + _router, + ); + final OnEnterResult onEnterResult = result is OnEnterResult + ? result + : await result; + + if (onEnterResult is Block) { + // When a route-level onEnter blocks, run onCanNotEnter to get final match list. + final OnEnterThenCallback? thenCb = onEnterResult.then; + final RouteMatchList matchList = await onCanNotEnter(); + + if (thenCb != null) { + try { + await Future.sync(thenCb); + } catch (error, stack) { + log('Error in then callback: $error'); + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'go_router', + context: ErrorDescription('while executing then callback'), + ), + ); + } + } + + return matchList; + } + } catch (error) { + final RouteMatchList errorMatchList = _errorRouteMatchList( + routeInformation.uri, + error is GoException ? error : GoException(error.toString()), + extra: infoState.extra, + ); + if (_onParserException != null && context.mounted) { + return _onParserException(context, errorMatchList); + } + return errorMatchList; + } + } + + return null; + } + /// Builds a [GoRouterState] based on the given [matchList]. /// /// This method derives the effective URI, full path, path parameters, and extra data from From 047704001f3f812b18e5ca82a446b863995f6d82 Mon Sep 17 00:00:00 2001 From: "berlis.b" Date: Mon, 26 Jan 2026 18:19:42 +0100 Subject: [PATCH 3/4] go_router: invoke onEnter during route parsing --- .github/PULL_REQUEST_TEMPLATE.md | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 541e9b112e5f..2489010ad2b2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -20,7 +20,6 @@ This PR adds support for route-level `onEnter` navigation guards in `GoRouteData - Code generation automatically handles `onEnter` method through existing `$route()` helper infrastructure ### Why - Resolves issue #181471 Currently, `go_router` 16.3.0 introduced a global `onEnter` callback at the `GoRouter` level. However, there's no way to define route-specific navigation guards in `GoRouteData` classes, unlike `redirect` and `onExit` which work at the route level. From c54cc6b5ac1407eeb4a8211ea855b3d0033f7886 Mon Sep 17 00:00:00 2001 From: "berlis.b" Date: Mon, 26 Jan 2026 20:30:58 +0100 Subject: [PATCH 4/4] go-route: Update `GoRoute`'s `onEnter` callback to use the `OnEnter` type and improve navigation state reset logic and unmounted context handling. --- .github/PULL_REQUEST_TEMPLATE.md | 83 +++------------------- packages/go_router/lib/src/parser.dart | 35 ++++++--- packages/go_router/lib/src/route.dart | 21 +----- packages/go_router/lib/src/route_data.dart | 2 +- 4 files changed, 38 insertions(+), 103 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 2489010ad2b2..93e584bd981c 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,80 +2,19 @@ *List which issues are fixed by this PR. You must list at least one issue.* -## Description - -This PR adds support for route-level `onEnter` navigation guards in `GoRouteData` classes for `go_router_builder`. - -### What changed - -**go_router:** -- Added `EnterCallback` typedef for route-level onEnter callbacks -- Added `onEnter` parameter to `GoRoute` constructor -- Added `onEnter` method to `_GoRouteDataBase` with default `Allow()` implementation -- Updated `_GoRouteParameters` to include `onEnter` callback -- Modified `GoRouteData.$route()` and `RelativeGoRouteData.$route()` helpers to pass `onEnter` to generated routes - -**go_router_builder:** -- Added test case `on_enter.dart` demonstrating usage of `onEnter` in `GoRouteData` classes -- Code generation automatically handles `onEnter` method through existing `$route()` helper infrastructure - -### Why -Resolves issue #181471 - -Currently, `go_router` 16.3.0 introduced a global `onEnter` callback at the `GoRouter` level. However, there's no way to define route-specific navigation guards in `GoRouteData` classes, unlike `redirect` and `onExit` which work at the route level. - -This PR enables developers to override the `onEnter` method in their `GoRouteData` classes to implement navigation guards for specific routes, providing consistency with the existing `onExit` pattern. - -### Example Usage - -```dart -@TypedGoRoute(path: '/protected') -class ProtectedRoute extends GoRouteData { - @override - FutureOr onEnter( - BuildContext context, - GoRouterState current, - GoRouterState next, - GoRouter router, - ) { - final isAuthenticated = checkAuth(); - if (!isAuthenticated) { - return Block.then(() => router.go('/login')); - } - return const Allow(); - } - - @override - Widget build(BuildContext context, GoRouterState state) { - return const ProtectedScreen(); - } -} -``` - -### Tests - -- All 389 existing tests in `go_router` pass -- All 40 tests in `go_router_builder` pass (including new `on_enter.dart` test case) -- New test file: `packages/go_router_builder/test_inputs/on_enter.dart` and `.expect` file - -### Version Updates - -- Updated `CHANGELOG.md` for both `go_router` and `go_router_builder` -- Version bumps should be handled by repository maintainers using the standard tooling - ## Pre-Review Checklist -- [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. -- [x] I read the [Tree Hygiene] page, which explains my responsibilities. -- [x] I read and followed the [relevant style guides] and ran [the auto-formatter]. -- [x] I signed the [CLA]. -- [x] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[go_router_builder]` and `[go_router]` -- [x] I [linked to at least one issue that this PR fixes] in the description above. -- [x] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under. -- [x] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under. -- [x] I updated/added any relevant documentation (doc comments with `///`). -- [x] I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under. -- [x] All existing and new tests are passing. +- [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. +- [ ] I read the [Tree Hygiene] page, which explains my responsibilities. +- [ ] I read and followed the [relevant style guides] and ran [the auto-formatter]. +- [ ] I signed the [CLA]. +- [ ] The title of the PR starts with the name of the package surrounded by square brackets, e.g. `[shared_preferences]` +- [ ] I [linked to at least one issue that this PR fixes] in the description above. +- [ ] I updated `pubspec.yaml` with an appropriate new version according to the [pub versioning philosophy], or I have commented below to indicate which [version change exemption] this PR falls under[^1]. +- [ ] I updated `CHANGELOG.md` to add a description of the change, [following repository CHANGELOG style], or I have commented below to indicate which [CHANGELOG exemption] this PR falls under[^1]. +- [ ] I updated/added any relevant documentation (doc comments with `///`). +- [ ] I added new tests to check the change I am making, or I have commented below to indicate which [test exemption] this PR falls under[^1]. +- [ ] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. diff --git a/packages/go_router/lib/src/parser.dart b/packages/go_router/lib/src/parser.dart index 8fb30ab593a8..acb7eb26a02e 100644 --- a/packages/go_router/lib/src/parser.dart +++ b/packages/go_router/lib/src/parser.dart @@ -190,8 +190,9 @@ class GoRouteInformationParser extends RouteInformationParser { ); } - // (Moved) Route-level onEnter handling is implemented as an instance method - // below within the [_OnEnterHandler] class so it can access instance state. + // Route-level onEnter handling is implemented as an instance method + // below within the [_OnEnterHandler] class. It was moved there from + // this class to better encapsulate the stateful redirection logic. /// Finds matching routes, processes redirects, and updates the route match /// list based on the navigation type. @@ -496,6 +497,8 @@ class _OnEnterHandler { extra: infoState.extra, ); + // Reset redirection history before handling the error to ensure the + // next navigation starts with a clean slate. _resetRedirectionHistory(); final bool canHandleException = @@ -526,12 +529,15 @@ class _OnEnterHandler { ); if (routeLevelBlocked != null) { - // A route-level onEnter blocked navigation; treat as blocked. + // Navigation was blocked at the route level; reset history before + // returning the "blocked" match list. matchList = routeLevelBlocked; _resetRedirectionHistory(); } else { matchList = await onCanEnter(); - _resetRedirectionHistory(); // reset after committed navigation + // Reset history after a successful navigation to clear state for + // the next navigation request. + _resetRedirectionHistory(); } } else { // Block: check if this is a hard stop or chaining block @@ -544,6 +550,8 @@ class _OnEnterHandler { // We intentionally don't try to detect "no-op" callbacks; any // Block with `then` keeps history so chained guards can detect loops. if (result.isStop) { + // Reset history on a hard stop (Block.stop) to ensure the next + // navigation attempt starts fresh. _resetRedirectionHistory(); } // For chaining blocks (with then), keep history to detect loops. @@ -569,7 +577,8 @@ class _OnEnterHandler { return matchList; }, onError: (Object error, StackTrace stackTrace) { - // Reset history on error to prevent stale state + // Reset history on error to prevent stale state from affecting + // subsequent navigation cycles. _resetRedirectionHistory(); final RouteMatchList errorMatchList = _errorRouteMatchList( @@ -627,8 +636,14 @@ class _OnEnterHandler { // Iterate in the same visitation order and invoke onEnter sequentially. for (final RouteMatchBase match in routeMatches) { if (!context.mounted) { - // If context is unmounted, avoid blocking navigation. - continue; + // If context is unmounted, abort the entire navigation. + return _errorRouteMatchList( + routeInformation.uri, + GoException( + 'Navigation aborted because the router context was disposed.', + ), + extra: infoState.extra, + ); } final RouteBase routeBase = match.route; if (routeBase is! GoRoute || routeBase.onEnter == null) { @@ -648,12 +663,12 @@ class _OnEnterHandler { if (onEnterResult is Block) { // When a route-level onEnter blocks, run onCanNotEnter to get final match list. - final OnEnterThenCallback? thenCb = onEnterResult.then; + final OnEnterThenCallback? thenCallback = onEnterResult.then; final RouteMatchList matchList = await onCanNotEnter(); - if (thenCb != null) { + if (thenCallback != null) { try { - await Future.sync(thenCb); + await Future.sync(thenCallback); } catch (error, stack) { log('Error in then callback: $error'); FlutterError.reportError( diff --git a/packages/go_router/lib/src/route.dart b/packages/go_router/lib/src/route.dart index e7ee23db48e9..fda21d443704 100644 --- a/packages/go_router/lib/src/route.dart +++ b/packages/go_router/lib/src/route.dart @@ -72,25 +72,6 @@ typedef NavigatorBuilder = typedef ExitCallback = FutureOr Function(BuildContext context, GoRouterState state); -/// Signature for function used in [GoRoute.onEnter]. -/// -/// This callback is invoked before entering a route and can be used to -/// implement navigation guards. It returns a [FutureOr] which -/// should resolve to [Allow] if navigation should proceed, or [Block] if -/// navigation should be prevented. -/// -/// The callback receives: -/// - [context]: The build context -/// - [current]: The current route state -/// - [next]: The route state being navigated to -/// - [router]: The GoRouter instance -typedef EnterCallback = - FutureOr Function( - BuildContext context, - GoRouterState current, - GoRouterState next, - GoRouter router, - ); /// The base class for [GoRoute] and [ShellRoute]. /// @@ -492,7 +473,7 @@ class GoRoute extends RouteBase { /// }, /// ), /// ``` - final EnterCallback? onEnter; + final OnEnter? onEnter; /// Determines whether the route matching is case sensitive. /// diff --git a/packages/go_router/lib/src/route_data.dart b/packages/go_router/lib/src/route_data.dart index 098d31b88391..81c1941a5be5 100644 --- a/packages/go_router/lib/src/route_data.dart +++ b/packages/go_router/lib/src/route_data.dart @@ -113,7 +113,7 @@ class _GoRouteParameters { final GoRouterPageBuilder pageBuilder; final GoRouterRedirect redirect; final ExitCallback onExit; - final EnterCallback onEnter; + final OnEnter onEnter; } /// Helper to create [GoRoute] parameters from a factory function and an Expando.