-
Notifications
You must be signed in to change notification settings - Fork 29.8k
Description
Is there an existing issue for this?
- I have searched the existing issues
- I have read the guide to filing a bug
Steps to reproduce
I have a GoRouter with parent ShellRoute wrapped in GestureDetector and two child routes. From the start first route works well. Or with context.go() method. But if I call context.replace() to go from first to the second route, the second route will be built without parent ShellRoute and GestureDetector. I think it is a bug and not the way the replacement should work.
Here's my code sample:
/// Key for root navigator.
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
/// Router of the app.
class AppRouter {
/// Router config.
static final GoRouter config = GoRouter(
initialLocation: '/${OnboardingScreen.tag}',
debugLogDiagnostics: kDebugMode,
routes: <RouteBase>[
ShellRoute(
navigatorKey: _rootNavigatorKey,
builder: (
final BuildContext context,
final GoRouterState state,
final Widget child,
) {
return MainResponsiveWrapper(
child: GestureDetector(
onTap: () {
FocusManager.instance.primaryFocus?.unfocus();
print('here');
},
child: child,
),
);
},
routes: <RouteBase>[
GoRoute(
path: '/${LoginScreen.tag}',
name: LoginScreen.tag,
pageBuilder: (
final BuildContext context,
final GoRouterState state,
) =>
const MaterialPage<LoginScreen>(child: LoginScreen()),
),
GoRoute(
path: '/${OnboardingScreen.tag}',
name: OnboardingScreen.tag,
pageBuilder: (
final BuildContext context,
final GoRouterState state,
) =>
const MaterialPage<OnboardingScreen>(child: OnboardingScreen()),
),
GoRoute(
path: '/',
redirect: (_, __) => '/${DepotScreen.tag}',
),
],
),
],
);Here are widget tree structure from dev tools:
As we can see OnboardingScreen is wrapped in GestureDetector, so the ShellRoute is here.

This one is after calling context.replace(). And here we can see that there is no GestureDetector and so on ShellRoute too. LoginScreen is wrapped in MaterialApp directly.

Expected results
I think it's expected that if we call context.replace() from one nested route, to another, the first route will be replaced, but not a parent ShellRoute, which should remain.
Actual results
As for the moment ShellRoute is being lost while replacing one of its children with another.
Code sample
I have prepared minimal reproducible example for the case (special thanks to this post):
Code sample
Just click a button with 'Click!' on the Home screen and you will see the ShellRoute is being lost after calling replace.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(const MyApp());
final GlobalKey<NavigatorState> rootNavKey = GlobalKey<NavigatorState>();
final GlobalKey<NavigatorState> shellNavKey = GlobalKey<NavigatorState>();
final GoRouter router = GoRouter(
navigatorKey: rootNavKey,
initialLocation: '/',
routes: [
GoRoute(
path: '/',
pageBuilder: (context, state) => const NoTransitionPage(child: Home()),
),
ShellRoute(
navigatorKey: shellNavKey,
routes: [
GoRoute(
path: '/somepage1',
pageBuilder: (context, state) => const NoTransitionPage(child: SomePage1()),
),
GoRoute(
path: '/somepage2',
pageBuilder: (context, state) => const NoTransitionPage(child: SomePage2()),
),
],
builder: (context, state, child) => GestureDetector(
onTap: () {
FocusManager.instance.primaryFocus?.unfocus();
print('here');
},
child: Shell(state: state, child: child),
),
),
],
);
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
class Shell extends StatelessWidget {
Shell({super.key, required this.state, required this.child}) {
print('Shell: ${state.location}');
}
final GoRouterState state;
final Widget child;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
Container(
width: 300.0,
color: Colors.black,
padding: const EdgeInsets.all(16),
child: Column(
children: [
ListTile(
onTap: () => context.go('/somepage1'),
title: const Text('Go to SomePage 1', style: TextStyle(color: Colors.white)),
),
ListTile(
onTap: () => context.go('/somepage2'),
title: const Text('Go to SomePage 2', style: TextStyle(color: Colors.white)),
),
],
),
),
Expanded(child: child),
],
),
);
}
}
class Home extends StatelessWidget {
const Home({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
onTap: () => context.replace('/somepage1'),
child: const Text('Click!'),
),
),
);
}
}
class SomePage1 extends StatelessWidget {
const SomePage1({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('Some Page 1'),
),
);
}
}
class SomePage2 extends StatelessWidget {
const SomePage2({super.key});
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('Some Page 2'),
),
);
}
}Flutter Doctor output
Doctor output
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.13.3, on macOS 13.5.2 22G91 darwin-arm64, locale ru-RU)
[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
[✓] Xcode - develop for iOS and macOS (Xcode 14.3.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.3)
[✓] IntelliJ IDEA Ultimate Edition (version 2023.2.1)
[✓] IntelliJ IDEA Community Edition (version 2022.2)
[✓] VS Code (version 1.82.0)
[✓] Connected device (3 available)
[✓] Network resources
• No issues found!