16

How to Use Shell Route with GoRoute in same hierarchy Routes

How do I push from ShellRoute to GoRoute in the same hierarchy as ShellRoute using one of the NavBar buttons?

Currently, the routes of GoRouter have two values: ShellRoute and GoRoute. The Child in ShellRoute represents Home, Discover, and Shop Widget. However, NavBar has one additional fourth button in addition to the three buttons to Widget mentioned above. This button (MY) is for going to the GoRoute mentioned earlier.

As you can see from the code to be attached below, I expected this to show the Login Widget when the 4th button is pressed on ShellRoute as the GoRoute is stacked on the ShellRoute. But in reality, as ShellRoute is removed, GoRoute becomes the top stack, iOS does not have a back button, and Android shuts down the app at BackPress.

There are no examples of this in the official document examples, so I'm lost a lot. I found a similar exampleten thousand https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/shell_route.dart In the example above, GoRoute is dependent on ShellRoute. I don't want this. I want to make the Login Route accessible from anywhere.

I will attach my code and operation screen below. I need your help.

Expected results: When the fourth button is pressed, the GoRoute is stacked on the ShellRoute, and the ShellRoute reappears when the back is pressed

Actual results: When the fourth button is pressed, ShellRoute is removed, creating a GoRoute and stacking it at the top, and the app ends when you press Back.

화면 기록 2022-11-30 오후 4 21 12 mov

Performing hot restart...
Syncing files to device iPhone 12...
Restarted application in 368ms.
flutter: MyTest didPush: _PageBasedMaterialPageRoute<void>(MaterialPage<void>("/", [<'280748962'>], {}), animation: AnimationController#0adb5(⏭ 1.000; paused; for _PageBasedMaterialPageRoute<void>(/)))
flutter: MyTest didPush: _PageBasedMaterialPageRoute<void>(MaterialPage<void>("/login", [<'621841910'>], {}), animation: AnimationController#4775c(▶ 0.000; for _PageBasedMaterialPageRoute<void>(/login)))
flutter: MyTest didRemove: _PageBasedMaterialPageRoute<void>(MaterialPage<void>("/shop", [<'280748962'>], {}), animation: AnimationController#0adb5(⏭ 1.000; paused; for _PageBasedMaterialPageRoute<void>(/)))
Code sample

main.dart

void main() {
  runApp(const MyApp());
}

final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

final _router = GoRouter(
  initialLocation: '/',
  navigatorKey: _rootNavigatorKey,
  observers: [
    GoRouterObserver(),
  ],
  routes: [
    ShellRoute(
        navigatorKey: _shellNavigatorKey,
        builder: (context, state, child) {
          return ScaffoldWithNavBar(child: child);
        },
        routes: [
          GoRoute(
            path: '/',
            builder: (context, state) {
              return const Home();
            },
          ),
          GoRoute(
            path: '/discover',
            builder: (context, state) {
              return const Discover();
            },
          ),
          GoRoute(
              path: '/shop',
              builder: (context, state) {
                return const Shop();
              }),
        ],
    ),
    GoRoute(
      path: '/login',
      builder: (context, state) {
        return const Login();
      },
    ),
  ],
);

class GoRouterObserver extends NavigatorObserver {
  @override
  void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
    print('MyTest didPush: $route');
  }

  @override
  void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
    print('MyTest didPop: $route');
  }

  @override
  void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
    print('MyTest didRemove: $route');
  }

  @override
  void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
    print('MyTest didReplace: $newRoute');
  }
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Platform',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routerConfig: _router,
    );
  }
}

scaffold_with_nav_bar.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

class ScaffoldWithNavBar extends StatefulWidget {
  const ScaffoldWithNavBar({super.key, required this.child});

  final Widget child;

  @override
  State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
}

class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
  int _currentIndex = 0;

  static const List<MyCustomBottomNavBarItem> tabs = [
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.home),
      activeIcon: Icon(Icons.home),
      label: 'HOME',
      initialLocation: '/',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.explore_outlined),
      activeIcon: Icon(Icons.explore),
      label: 'DISCOVER',
      initialLocation: '/discover',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.storefront_outlined),
      activeIcon: Icon(Icons.storefront),
      label: 'SHOP',
      initialLocation: '/shop',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.account_circle_outlined),
      activeIcon: Icon(Icons.account_circle),
      label: 'MY',
      initialLocation: '/login',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    const labelStyle = TextStyle(fontFamily: 'Roboto');
    return Scaffold(
      body: SafeArea(child: widget.child),
      bottomNavigationBar: BottomNavigationBar(
        selectedLabelStyle: labelStyle,
        unselectedLabelStyle: labelStyle,
        selectedItemColor: const Color(0xFF434343),
        selectedFontSize: 12,
        unselectedItemColor: const Color(0xFF838383),
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
        onTap: (int index) {
          _goOtherTab(context, index);
        },
        currentIndex: _currentIndex,
        items: tabs,
      ),
    );
  }

  void _goOtherTab(BuildContext context, int index) {
    if (index == _currentIndex) return;
    GoRouter router = GoRouter.of(context);
    String location = tabs[index].initialLocation;
    if (index == 3) {
      router.push(location);
    }

    setState(() {
      _currentIndex = index;
      router.go(location);
    });
  }
}

class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
  final String initialLocation;

  const MyCustomBottomNavBarItem(
      {required this.initialLocation,
      required Widget icon,
      String? label,
      Widget? activeIcon})
      : super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
}
Logs
 info • Avoid `print` calls in production code • lib/main.dart:62:5 • avoid_print
   info • Avoid `print` calls in production code • lib/main.dart:67:5 • avoid_print
   info • Avoid `print` calls in production code • lib/main.dart:72:5 • avoid_print
   info • Avoid `print` calls in production code • lib/main.dart:77:5 • avoid_print
   info • Prefer const with constant constructors • lib/sections/discover/discover.dart:11:16 • prefer_const_constructors
   info • Avoid using private types in public APIs • lib/sections/exhibition/detail_exhibition.dart:10:3 • library_private_types_in_public_api
   info • Prefer const with constant constructors • lib/sections/exhibition/exhibition.dart:11:16 • prefer_const_constructors
   info • Unused import: 'package:knowd_platform/sections/discover/discover.dart' • lib/sections/home/home.dart:2:8 • unused_import
   info • Prefer const with constant constructors • lib/sections/home/home.dart:21:15 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/login/login.dart:11:16 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/login/login_router.dart:8:34 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/login/singup.dart:11:16 • prefer_const_constructors
   info • Avoid using private types in public APIs • lib/sections/main_tab_bar.dart:12:3 • library_private_types_in_public_api
   info • Private field could be final • lib/sections/main_tab_bar.dart:17:16 • prefer_final_fields
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:18:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:19:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:20:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:21:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:41:18 • prefer_const_constructors
   info • Prefer const literals as parameters of constructors on @immutable classes • lib/sections/main_tab_bar.dart:59:17 • prefer_const_literals_to_create_immutables
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:60:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:61:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:64:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:65:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:68:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:69:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:72:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:73:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/shop.dart:10:16 • prefer_const_constructors
 info • Avoid `print` calls in production code • lib/main.dart:62:5 • avoid_print
   info • Avoid `print` calls in production code • lib/main.dart:67:5 • avoid_print
   info • Avoid `print` calls in production code • lib/main.dart:72:5 • avoid_print
   info • Avoid `print` calls in production code • lib/main.dart:77:5 • avoid_print
   info • Prefer const with constant constructors • lib/sections/discover/discover.dart:11:16 • prefer_const_constructors
   info • Avoid using private types in public APIs • lib/sections/exhibition/detail_exhibition.dart:10:3 • library_private_types_in_public_api
   info • Prefer const with constant constructors • lib/sections/exhibition/exhibition.dart:11:16 • prefer_const_constructors
   info • Unused import: 'package:knowd_platform/sections/discover/discover.dart' • lib/sections/home/home.dart:2:8 • unused_import
   info • Prefer const with constant constructors • lib/sections/home/home.dart:21:15 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/login/login.dart:11:16 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/login/login_router.dart:8:34 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/login/singup.dart:11:16 • prefer_const_constructors
   info • Avoid using private types in public APIs • lib/sections/main_tab_bar.dart:12:3 • library_private_types_in_public_api
   info • Private field could be final • lib/sections/main_tab_bar.dart:17:16 • prefer_final_fields
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:18:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:19:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:20:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:21:5 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:41:18 • prefer_const_constructors
   info • Prefer const literals as parameters of constructors on @immutable classes • lib/sections/main_tab_bar.dart:59:17 • prefer_const_literals_to_create_immutables
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:60:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:61:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:64:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:65:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:68:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:69:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:72:13 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/main_tab_bar.dart:73:21 • prefer_const_constructors
   info • Prefer const with constant constructors • lib/sections/shop.dart:10:16 • prefer_const_constructors

krishnaacharyaa
  • 14,953
  • 4
  • 49
  • 88
김상현
  • 268
  • 1
  • 2
  • 10

1 Answers1

29

Things to consider while using context.go() from ShellRoute to GoRoute

  1. Specify parentNavigatorKey prop in each GoRoute
    • If page is child of ShellRoute : parentNavigatorKey:_shellNavigatorKey
    • If page is child of MainRoute : parentNavigatorKey:_rootNavigatorKey
  2. Use context.go() to replace page , context.push() to push page to stack

Code Structure to follow:


final _parentKey = GlobalKey<NavigatorState>();
final _shellKey = GlobalKey<NavigatorState>();

|_ GoRoute
  |_ parentNavigatorKey = _parentKey    Specify key here
|_ ShellRoute
  |_ GoRoute                            // Needs Bottom Navigation                  
    |_ parentNavigatorKey = _shellKey   
  |_ GoRoute                            // Needs Bottom Navigation
    |_ parentNavigatorKey = _shellKey   
|_ GoRoute                              // Full Screen which doesn't need Bottom Navigation
  |_parentNavigatorKey = _parentKey

Mistakes in your code

  1. Not specifying parentNavigatorKey

  2. For backButton to be visible you have to have appBar inside Scaffold.

  3. This code

    if (index == 3) {
     router.push(location);
     }
     setState(() {
     _currentIndex = index;
     router.go(location);
     });
    }  
    

Code improvements made

  1. Visible BackButton
  2. Navigate Back to ShellRoute
  3. Persist the Navigation menu on clicking back button
  4. Fixed weird transition between routes when using navbar

Code:

Router


final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorKey = GlobalKey<NavigatorState>();

final router = GoRouter(
  initialLocation: '/',
  navigatorKey: _rootNavigatorKey,
  routes: [
    ShellRoute(
      navigatorKey: _shellNavigatorKey,
      pageBuilder: (context, state, child) {
        print(state.location);
        return NoTransitionPage(
            child: ScaffoldWithNavBar(
          location: state.location,
          child: child,
        ));
      },
      routes: [
        GoRoute(
          path: '/',
          parentNavigatorKey: _shellNavigatorKey,
          pageBuilder: (context, state) {
            return const NoTransitionPage(
              child: Scaffold(
                body: Center(child: Text("Home")),
              ),
            );
          },
        ),
        GoRoute(
          path: '/discover',
          parentNavigatorKey: _shellNavigatorKey,
          pageBuilder: (context, state) {
            return const NoTransitionPage(
              child: Scaffold(
                body: Center(child: Text("Discover")),
              ),
            );
          },
        ),
        GoRoute(
            parentNavigatorKey: _shellNavigatorKey,
            path: '/shop',
            pageBuilder: (context, state) {
              return const NoTransitionPage(
                child: Scaffold(
                  body: Center(child: Text("Shop")),
                ),
              );
            }),
      ],
    ),
    GoRoute(
      parentNavigatorKey: _rootNavigatorKey,
      path: '/login',
      pageBuilder: (context, state) {
        return NoTransitionPage(
          key: UniqueKey(),
          child: Scaffold(
            appBar: AppBar(),
            body: const Center(
              child: Text("Login"),
            ),
          ),
        );
      },
    ),
  ],
);

BottomNavigationBar

class ScaffoldWithNavBar extends StatefulWidget {
  String location;
  ScaffoldWithNavBar({super.key, required this.child, required this.location});

  final Widget child;

  @override
  State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState();
}

class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> {
  int _currentIndex = 0;

  static const List<MyCustomBottomNavBarItem> tabs = [
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.home),
      activeIcon: Icon(Icons.home),
      label: 'HOME',
      initialLocation: '/',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.explore_outlined),
      activeIcon: Icon(Icons.explore),
      label: 'DISCOVER',
      initialLocation: '/discover',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.storefront_outlined),
      activeIcon: Icon(Icons.storefront),
      label: 'SHOP',
      initialLocation: '/shop',
    ),
    MyCustomBottomNavBarItem(
      icon: Icon(Icons.account_circle_outlined),
      activeIcon: Icon(Icons.account_circle),
      label: 'MY',
      initialLocation: '/login',
    ),
  ];

  @override
  Widget build(BuildContext context) {
    const labelStyle = TextStyle(fontFamily: 'Roboto');
    return Scaffold(
      body: SafeArea(child: widget.child),
      bottomNavigationBar: BottomNavigationBar(
        selectedLabelStyle: labelStyle,
        unselectedLabelStyle: labelStyle,
        selectedItemColor: const Color(0xFF434343),
        selectedFontSize: 12,
        unselectedItemColor: const Color(0xFF838383),
        showUnselectedLabels: true,
        type: BottomNavigationBarType.fixed,
        onTap: (int index) {
          _goOtherTab(context, index);
        },
        currentIndex: widget.location == '/'
            ? 0
            : widget.location == '/discover'
                ? 1
                : widget.location == '/shop'
                    ? 2
                    : 3,
        items: tabs,
      ),
    );
  }

  void _goOtherTab(BuildContext context, int index) {
    if (index == _currentIndex) return;
    GoRouter router = GoRouter.of(context);
    String location = tabs[index].initialLocation;

    setState(() {
      _currentIndex = index;
    });
    if (index == 3) {
      context.push('/login');
    } else {
      router.go(location);
    }
  }
}

class MyCustomBottomNavBarItem extends BottomNavigationBarItem {
  final String initialLocation;

  const MyCustomBottomNavBarItem(
      {required this.initialLocation,
      required Widget icon,
      String? label,
      Widget? activeIcon})
      : super(icon: icon, label: label, activeIcon: activeIcon ?? icon);
}

Output:

enter image description here

enter image description here

krishnaacharyaa
  • 14,953
  • 4
  • 49
  • 88
  • I have the exact same problem, your solution didn't work until I added a `UniqueKey` to the top route being pushed above the shell route, does this have anything to do with the problem? – HII May 12 '23 at 10:26
  • @HII can you share your solution here or in a github gist? I've been stuck for a long time with this problem – yshean Aug 15 '23 at 13:11