skip to Main Content

I am trying to achieve a persistent menu (side-bar) in flutter web. I can achieve that as long as all the pages in the app are directly listed as menu items.
The problem is I cannot get the nested pages (pages not listed in the side menu but open on some button click from inside another page) to open in the "content area".

I have tried a LOT of stuff from Navigation Rail to GoRouter to different packages for sidebar.

I don’t know what code to post here.

My project also uses Getx.

With whatever code I have I can open 1st level pages in the content area but the nested pages load as a new page and the side-bar is lost.

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

        @override
        Widget build(BuildContext context) {
          ///
          final MyMenuController menuController = Get.put(MyMenuController());

          ///
          return Scaffold(
            appBar: AppBar(
              title: const Text('Side Menu Example'),
            ),
            body: Row(
              children: [
                Expanded(
                  child: Container(
                    color: Colors.blue,
                    child: ListView(
                      children: [
                        ListTile(
                          title: const Text('Page 1'),
                          selected: menuController.selectedIndex.value == 0,
                          onTap: () {
                            menuController.selectTab(0);
                          },
                        ),
                        ListTile(
                          title: const Text('Page 2'),
                          selected: menuController.selectedIndex.value == 1,
                          onTap: () {
                            menuController.selectTab(1);
                          },
                        ),
                        ListTile(
                          title: const Text('Page 3'),
                          selected: menuController.selectedIndex.value == 2,
                          onTap: () {
                            menuController.selectTab(2);
                          },
                        ),
                      ],
                    ),
                  ),
                ),
                Expanded(
                  flex: 5,
                  child: Container(color: Colors.white, child: const SizedBox() //

                      Obx(
                        () {
                          switch (menuController.selectedIndex.value) {
                            case 0:
                              return const Page1();
                            case 1:
                              return const Page2();
                            case 2:
                              return const Page3();
                            default:
                              return Container();
                            return Container();
                          }
                        },
                      ),
                      ),
                ),
              ],
            ),
            // ),
          );
        }
      }

Just in case somebody wants to say "Show us what you have tried". I have tried a bunch of stuff nothing worked out so far. If you know how to achieve this, please point me in the right direction.

2

Answers


  1. To know first

    You need first to understand that the Navigator widget lists all opened routes as "siblings", when you do have a Page1 that is a route in your Navigator, you can think of it like it is a child of the Navigator when you open another route of that Navigator, it is opened as a sibling for that Page2.

    The Navigator widget is an InheritedWidget, which means that using Getx for routing disables the interaction with the BuildContext
    of it (with Get.to(Widget()) as an example ), which you need to avoid/not use.

                  ---> Page1---> Button1 ( that opens Page2 route )
    Navigator => |
                  ---> Page2
    

    How this relates to your case?

    by understanding this, if you have a menu bar in Page1, it will not be shown in Page2 since it is a different route, which is why you get that behavior in your app.

    How to fix/achieve your expected behavior?

    the Navigator widget is an InheritedWidget, which means that to get access to it from your app, we use Navigator.of(context), this gets up the Navigator widget that is contained by default in the MaterialApp of our app.

    for having an app that will always show the persistent navigation bar even if we do navigate to nested routes in the app, we need to create a new Navigator widget that is upfront the screen which has the menu bar.

                                          ---> Page1 ---> Button1 ( that opens Page2 route )
       ScreenWithMenuBar -> Navigator => |
                                          ---> Page2
    

    so when you navigate between pages, always the ScreenWithMenuBar will be shown.

    Login or Signup to reply.
  2. I am using GoRouter as of a couple of days and still trying to understand some key concepts but this is how I managed to implement a nested navigation.

    == Router ==

    • not a big fan of having all the routes into one single file ( coming from Beamer where you would set a location for each screen so this part might need refactoring )
    • the magic happens in ShellRoute as that returns the RootLayout where I have an AdaptiveNavigator and expects a child that is the screen you want to display
    • you basically have 2 navigators hence the 2 keys / state and each handle the state for the navigation and the state of the screen
    • if you have screens where you don’t wish to display the navigator you need to provide the _parentKey to parentNavigatorKey
    • looking at best practices it seems that top level pages should not have transition animations and only when you navigation deeper => list of records would be displayed without an animation whilst details of a record screen would have CupertinoPage, MaterialPage or CustomTransitionPage where you get to add your own magic
    final _parentKey = GlobalKey<NavigatorState>();
    final _shellKey = GlobalKey<NavigatorState>();
    
    final appRouter = GoRouter(
      // debugLogDiagnostics: true,
      navigatorKey: _parentKey,
      initialLocation: Routes.login,
      redirect: (BuildContext context, GoRouterState state) {
        final bool isAuthenticated =
            context.read<AuthenticationBloc>().isAuthenticated;
        if (state.subloc == Routes.register && !isAuthenticated) {
          return Routes.register;
        } else if (state.subloc == Routes.login && !isAuthenticated) {
          return Routes.login;
        } else if (state.subloc == Routes.login && isAuthenticated) {
          return Routes.dashboard;
        } else if (!isAuthenticated) {
          return Routes.login;
        } else {
          return null;
        }
      },
      // TODO => create page not found screen
      // * this is mainly used for the web version when you hardlink to an unknown page
      errorBuilder: ((context, state) => const Scaffold(
            body: Center(
              child: Text(
                'This my custom errorpage',
                textAlign: TextAlign.center,
              ),
            ),
          )),
      routes: [
        ShellRoute(
          // * => shell route for the navigation bar
          navigatorKey: _shellKey,
          builder: (context, state, child) {
            return RootLayout(child: child);
          },
          routes: [
            GoRoute(
              name: 'dashboard',
              path: Routes.dashboard,
              pageBuilder: (context, state) => NoTransitionPage(
                key: state.pageKey,
                child: const DashboardPage(),
              ),
            ),
            // * => documents route + sub routes
            GoRoute(
              name: 'documents',
              path: Routes.documents,
              pageBuilder: (context, state) => NoTransitionPage(
                key: state.pageKey,
                child: const DocumentsPage(),
              ),
              routes: [
                GoRoute(
                  parentNavigatorKey: _parentKey,
                  name: 'documentsView',
                  path: Routes.documentsView,
                  pageBuilder: (context, state) => CupertinoPage(
                    key: state.pageKey,
                    child: ViewDocumentPage(
                      documentUid: state.params['documentUid'] as String,
                    ),
                  ),
                ),
                GoRoute(
                  parentNavigatorKey: _parentKey,
                  name: 'documentsEdit',
                  path: Routes.documentsEdit,
                  pageBuilder: (context, state) => CupertinoPage(
                    key: state.pageKey,
                    child: EditDocumentPage(
                      documentUid: state.params['documentUid'] as String,
                    ),
                  ),
                ),
                GoRoute(
                  parentNavigatorKey: _parentKey,
                  name: 'documentsAdd',
                  path: Routes.documentsAdd,
                  pageBuilder: (context, state) => CupertinoPage(
                    key: state.pageKey,
                    child: const AddDocumentPage(),
                  ),
                ),
              ],
            ),
            // * => websites route + sub routes
            GoRoute(
              name: 'websites',
              path: Routes.websites,
              pageBuilder: (context, state) => NoTransitionPage(
                key: state.pageKey,
                child: const WebsitesPage(),
              ),
              routes: [
                GoRoute(
                  parentNavigatorKey: _parentKey,
                  name: 'websitesAdd',
                  path: Routes.websitesAdd,
                  pageBuilder: (context, state) => CupertinoPage(
                    key: state.pageKey,
                    child: const AddWebsitePage(),
                  ),
                ),
              ],
            ),
            // * => tasks route + sub routes
            GoRoute(
              name: 'tasks',
              path: Routes.tasks,
              pageBuilder: (context, state) => NoTransitionPage(
                key: state.pageKey,
                child: const ListTasksPage(),
              ),
              routes: [
                GoRoute(
                  parentNavigatorKey: _parentKey,
                  name: 'tasksAdd',
                  path: Routes.tasksAdd,
                  pageBuilder: (context, state) => CupertinoPage(
                    key: state.pageKey,
                    child: const AddTaskPage(),
                  ),
                ),
              ],
            ),
          ],
        ),
        // * => Auth
        GoRoute(
          name: 'login',
          parentNavigatorKey: _parentKey,
          path: Routes.login,
          pageBuilder: (context, state) => CupertinoPage(
            key: state.pageKey,
            child: const LoginPage(),
          ),
        ),
        GoRoute(
          name: 'register',
          parentNavigatorKey: _parentKey,
          path: Routes.register,
          pageBuilder: (context, state) => CupertinoPage(
            key: state.pageKey,
            child: const RegisterPage(),
          ),
        ),
        // * => search
        GoRoute(
          name: 'search',
          parentNavigatorKey: _parentKey,
          path: Routes.search,
          pageBuilder: (context, state) => CupertinoPage(
            key: state.pageKey,
            child: const SearchPage(),
          ),
        ),
      ],
    );
    

    == RootLayout ==

    • AdaptiveNavigation simply checks layout size and if Desktop it will display a NavigationRail or Mobile BottomNavigationBar
    • _Switcher is just a class that provides some basic animation when the layout size changes
    class RootLayout extends StatelessWidget {
      const RootLayout({
        Key? key,
        required this.child,
      }) : super(key: key);
    
          final Widget child;
          static const _switcherKey = ValueKey('switcherKey');
        
          @override
          Widget build(BuildContext context) {
            return AdaptiveNavigation(
              child: _Switcher(
                key: _switcherKey,
                child: child,
              ),
            );
          }
        }
        
        class _Switcher extends StatelessWidget {
          final Widget child;
        
          const _Switcher({
            required this.child,
            super.key,
          });
        
          @override
          Widget build(BuildContext context) {
            return UniversalPlatform.isDesktop
            ? child : AnimatedSwitcher(
            key: key,
            duration: const Duration(milliseconds: 200),
            switchInCurve: Curves.easeInOut,
            switchOutCurve: Curves.easeInOut,
            child: child,
            );
          }
        }
    

    Overall what is happening here is one router with 2 states that has as root a widget with a navbar and a child Widget that is being passed down from GoRouter.
    Please let me know if you require any further details.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search