Skip to content

[go_router] Incorrect work of context.replace() method #134524

@abigotado

Description

@abigotado

Is there an existing issue for this?

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.
Снимок экрана 2023-09-12 в 08 18 17

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.
Снимок экрана 2023-09-12 в 08 23 12

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!

Metadata

Metadata

Assignees

Labels

P2Important issues not at the top of the work listfound in release: 3.13Found to occur in 3.13found in release: 3.14Found to occur in 3.14has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.r: fixedIssue is closed as already fixed in a newer version

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions