How to return to a nested route when switching between tabs with ShellRoute and GoRouter 5.0?
Asked Answered
D

2

17

I’m trying to get nested navigation to work with ShellRoute in go_router 5.0.

As the example below shows, I can correctly navigate to the (nested) product page when clicking on a product.

But if I switch to the cart tab and back to products, I'm taken back to the (root) products page rather than the (nested) product page:

ShellRoute example with GoRouter 5.0

This is how I've setup my router:

enum AppRoute {
  products,
  product,
  cart,
  account,
}

final goRouter = GoRouter(
    initialLocation: '/products',
    navigatorKey: _rootNavigatorKey,
    debugLogDiagnostics: true,
    routes: [
      ShellRoute(
        navigatorKey: _shellNavigatorKey,
        builder: (context, state, child) {
          return ScaffoldWithBottomNavBar(child: child);
        },
        routes: [
          // Products
          GoRoute(
            path: '/products',
            name: AppRoute.products.name,
            redirect: (context, state) {
              print(state.toString());
              return null;
            },
            pageBuilder: (context, state) => NoTransitionPage(
              key: state.pageKey,
              restorationId: state.pageKey.value,
              child: const ProductsListScreen(),
            ),
            routes: [
              GoRoute(
                path: ':id',
                name: AppRoute.product.name,
                pageBuilder: (context, state) {
                  final productId = state.params['id']!;
                  // TODO: Cupertino slide transition
                  return MaterialPage(
                    key: state.pageKey,
                    restorationId: state.pageKey.value,
                    child: ProductScreen(productId: productId),
                  );
                },
              ),
            ],
          ),
          // Shopping Cart
          GoRoute(
            path: '/cart',
            name: AppRoute.cart.name,
            pageBuilder: (context, state) => NoTransitionPage(
              key: state.pageKey,
              child: const ShoppingCartScreen(),
            ),
          ),
          // Account page
          GoRoute(
            path: '/account',
            name: AppRoute.account.name,
            pageBuilder: (context, state) => NoTransitionPage(
              key: state.pageKey,
              child: const AccountScreen(),
            ),
          ),
        ],
      ),
    ],
  );

This is the build method of the ScaffoldWithBottomNavBar widget:

@override
  Widget build(BuildContext context) {
    return Scaffold(
      body: widget.child,
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        unselectedItemColor: Colors.grey,
        selectedItemColor: Colors.black87,
        currentIndex: _selectedIndex,
        items: [
          BottomNavigationBarItem(
            icon: const Icon(
              Icons.list,
            ),
            label: 'Products'.hardcoded,
          ),
          BottomNavigationBarItem(
            icon: const ShoppingCartIcon(),
            label: 'Cart'.hardcoded,
          ),
          BottomNavigationBarItem(
            icon: const Icon(
              Icons.account_circle,
            ),
            label: 'Account'.hardcoded,
          ),
        ],
        onTap: (index) => _tap(context, index),
      ),
    );
  }

In the tab button callback, I do this:

  void _tap(BuildContext context, int index) {
    setState(() => _selectedIndex = index); // used for the highlighted state
    // navigate to the target route based on the tab index
    if (index == 0) {
      context.goNamed(AppRoute.products.name);
    } else if (index == 1) {
      context.goNamed(AppRoute.cart.name);
    } else if (index == 2) {
      context.goNamed(AppRoute.account.name);
    }
  }

I understand why this does not work since I’m telling GoRouter to go to each of the (root) routes inside the ShellRoute.

Ideally, I'd want a way to switch to a specific tab by index, and have GoRouter "remember" if it was on a nested or top-level route.

Note that unlike TabBar, BottomNavigationBar does not have a controller I can use to switch the index.

GoRouter 5.0 is new and there isn't much documentation or examples showing these kind of common use cases.

Anyone knows how to approach this?

Note: one of my ideas was to store the selected product ID as application state somewhere, and use it inside a redirect function inside the /products route, but then it becomes complex to know where I should reset that state to null, and my experiments with NavigationObserver did not yield the desired result.

Update 28th Jun 2023

GoRouter 7.1.0 added a new StatefulShellRoute class to support this.

I've written a full tutorial explaining how to use it:

Discourtesy answered 21/9, 2022 at 9:9 Comment(1)
hello, how are you able to pass the data from your ProductsListScreen to the ProductScreen route without any loading ?Dripstone
L
6

Sadly this is the missing piece to make it the perfect router. As you noticed this is the issue to track and the team is already on it: https://github.com/flutter/flutter/issues/99124.

Lowrie answered 21/9, 2022 at 18:15 Comment(0)
A
0

You can use StatefulSheelRoute instead of ShellRoute

Attractive answered 26/10, 2023 at 18:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.