Steps to reproduce
- Create two similar routes and enable
debugLogDiagnostics in your GoRouter config:
// Generated route
@TypedGoRoute<MyGeneratedRoute>(path: '/generated')
class MyGeneratedRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return MyPage();
}
}
// Manual route
GoRoute(
path: '/manual',
builder: (context, state) => MyPage(),
// Note: no onExit callback
)
- Navigate to each route and then pop back
- Observe the timing difference in when route configuration updates occur and the popping and restoring logs.
Expected results
Both generated and manual routes should have consistent timing for route configuration updates during navigation operations.
Actual results
There's a significant behavioral difference between routes created with GoRouteData.$route (generated routes) and manually defined GoRoutes in how they handle route popping, specifically in the _handlePopPageWithRouteMatch method. This leads to different timing for when _completeRouteMatch is called, which can cause issues with state management and route configuration updates. Mostly on pop() where restore(currentConfiguration)will be called before _completeRouteMatch execution.
Root Cause Analysis
In delegate.dart at line 152-171, the logic branches based on whether routeBase.onExit == null:
bool _handlePopPageWithRouteMatch(
Route<Object?> route,
Object? result,
RouteMatchBase match,
) {
// ... willHandlePopInternally check ...
final RouteBase routeBase = match.route;
if (routeBase is! GoRoute || routeBase.onExit == null) {
// MANUAL ROUTES: Execute immediately
route.didPop(result);
_completeRouteMatch(result, match); // ← Synchronous execution
return true;
}
// GENERATED ROUTES: Always go through this path
scheduleMicrotask(() async {
final bool onExitResult = await routeBase.onExit!(
navigatorKey.currentContext!,
match.buildState(_configuration, currentConfiguration),
);
if (onExitResult) {
_completeRouteMatch(result, match); // ← Asynchronous execution
}
});
return false;
}
Code sample
main.dart
void main() {
runApp(
ProviderScope(
child: MaterialApp.router(
routerConfig: router,
),
),
);
}
class GroupsPage extends ConsumerWidget {
const GroupsPage({super.key, required this.groupId});
final String groupId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
title: Text('Group $groupId'),
actions: [
IconButton(
onPressed: () => context.go('/groups/$groupId/settings'),
icon: const Icon(Icons.settings),
),
],
),
body: ListView.builder(
itemCount: 10,
itemBuilder: (context, index) => ListTile(
onTap: () => context.go('/groups/$groupId/posts/$index'),
tileColor: [Colors.red.shade300, Colors.blue.shade300][index % 2],
title: Text('Post of group $groupId n°$index'),
),
),
);
}
}
class PostPage extends ConsumerWidget {
const PostPage({super.key, required this.postId});
final String postId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back),
),
title: Text('Post $postId'),
),
body: Text('This is the content of post $postId'),
);
}
}
class SettingsPage extends ConsumerWidget {
const SettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back),
),
),
body: const Center(
child: Column(
children: [
Text('Settings'),
],
),
),
);
}
}
router.dart
part 'router.g.dart';
final router = GoRouter(
debugLogDiagnostics: true,
initialLocation: '/groups/1',
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, child) => Scaffold(
appBar: AppBar(
title: const Text('Test App'),
),
body: child,
),
branches: [
StatefulShellBranch(
routes: [
ShellRoute(
builder: (context, state, child) => Scaffold(
appBar: AppBar(
title: const Text('Group List'),
),
body: child,
),
routes: [
GoRoute(
path: '/groups/empty',
builder: (context, state) => const GroupsPage(
groupId: 'all',
),
redirect: (context, state) => '/groups/1',
),
GoRoute(
path: '/groups/:id',
builder: (context, state) => GroupsPage(
groupId: state.pathParameters['id']!,
),
routes: [
PostDetailPage.route,
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsPage(),
),
],
),
],
),
],
)
],
)
],
);
@TypedGoRoute<PostDetailPage>(path: 'posts/:id')
class PostDetailPage extends GoRouteData with _$PostDetailPage {
const PostDetailPage({required this.id});
static final route = $postDetailPage;
final String id;
@override
Widget build(BuildContext context, GoRouterState state) {
return PostPage(
postId: id,
);
}
}
Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
[GoRouter] Full paths for routes:
└─ (ShellRoute)
└─ (ShellRoute)
├─/groups/empty (GroupsPage)
└─/groups/:id (GroupsPage)
├─/groups/:id/posts/:id (Widget)
└─/groups/:id/settings (SettingsPage)
[GoRouter] setting initial location /groups/1
[GoRouter] Using MaterialApp configuration
[GoRouter] going to /groups/1/settings
[GoRouter] popping /groups/1/settings
[GoRouter] restoring /groups/1
[GoRouter] going to /groups/1/posts/0
[GoRouter] popping /groups/1/posts/0
[GoRouter] restoring /groups/1/posts/0
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.35.1, on macOS 15.4.1 24E263 darwin-arm64, locale fr-FR) [277ms]
• Flutter version 3.35.1 on channel stable
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision 20f8274939 (13 days ago), 2025-08-14 10:53:09 -0700
• Engine revision 1e9a811bf8
• Dart version 3.9.0
• DevTools version 2.48.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android,
enable-ios, cli-animations, enable-lldb-debugging
Steps to reproduce
debugLogDiagnosticsin your GoRouter config:Expected results
Both generated and manual routes should have consistent timing for route configuration updates during navigation operations.
Actual results
There's a significant behavioral difference between routes created with GoRouteData.$route (generated routes) and manually defined GoRoutes in how they handle route popping, specifically in the
_handlePopPageWithRouteMatchmethod. This leads to different timing for when_completeRouteMatchis called, which can cause issues with state management and route configuration updates. Mostly onpop()whererestore(currentConfiguration)will be called before_completeRouteMatchexecution.Root Cause Analysis
In delegate.dart at line 152-171, the logic branches based on whether routeBase.onExit == null:
Code sample
main.dart
router.dart
Screenshots or Video
Screenshots / Video demonstration
[Upload media here]
Logs
Logs
Flutter Doctor output
Doctor output