Search code examples
iosflutterdartroutesflutter-go-router

How to work with NavigationBar in go_router? | Flutter


I am currently struggling refactoring my routing code with go_router.

I already got some simple routes like /signin & /signup, but the problem comes in when I try to make the routing work with a BottomNavigationBar that has multiple screens. I would like to have a separate route for each of them like /home, /events & /profile.

I figured out that I somehow have to return the same widget with a different parameter to prevent the whole screen to change whenever a BottomNavigationBarItem is pressed and instead only update the part above the BottomNavigationBar which would be the screen itself.

I came up with a pretty tricky solution:

GoRoute(
  path: '/:path',
  builder: (BuildContext context, GoRouterState state) {
    final String path = state.params['path']!;

    if (path == 'signin') {
      return const SignInScreen();
    }

    if (path == 'signup') {
      return const SignUpScreen();
    }

    if (path == 'forgot-password') {
      return const ForgotPasswordScreen();
    }

    // Otherwise it has to be the ScreenWithBottomBar

    final int index = getIndexFromPath(path);

    if (index != -1) {
      return MainScreen(selectedIndex: index);
    }

    return const ErrorScreen();
  }
)

This does not look very good and it makes it impossible to add subroutes like /profile/settings/appearance or /events/:id.

I would like to have something easy understandable like this:

GoRoute(
  path: '/signin',
  builder: (BuildContext context, GoRouterState state) {
    return const SignInScreen();
  }
),
GoRoute(
  path: '/signup',
  builder: (BuildContext context, GoRouterState state) {
    return const SignUpScreen();
  }
),
GoRoute(
  path: '/home',
  builder: (BuildContext context, GoRouterState state) {
    return const ScreenWithNavBar(selectedScreen: 1);
  }
),
GoRoute(
  path: '/events',
  builder: (BuildContext context, GoRouterState state) {
    return const ScreenWithNavBar(selectedScreen: 2);
  },
  routes: <GoRoute>[
    GoRoute(
      path: ':id',
      builder: (BuildContext context, GoRouterState state) {
        return const EventScreen();
      }
    )
  ]
)

Is there any way to achieve the behavior?


Solution

  • Here is full working example, from latest go_router changes, with ShellRoute:

    import 'package:flutter/material.dart';
    import 'package:go_router/go_router.dart';
    
    Future<void> main() async {
      runApp(
        StatefulShellRouteExampleApp(),
      );
    }
    
    class ScaffoldBottomNavigationBar extends StatelessWidget {
      const ScaffoldBottomNavigationBar({
        required this.navigationShell,
        Key? key,
      }) : super(key: key ?? const ValueKey<String>('ScaffoldBottomNavigationBar'));
    
      final StatefulNavigationShell navigationShell;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: navigationShell,
          bottomNavigationBar: BottomNavigationBar(
            items: const <BottomNavigationBarItem>[
              BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Section_A'),
              BottomNavigationBarItem(icon: Icon(Icons.work), label: 'Section_B'),
            ],
            currentIndex: navigationShell.currentIndex,
            onTap: (int tappedIndex) {
              navigationShell.goBranch(tappedIndex);
            },
          ),
        );
      }
    }
    
    class StatefulShellRouteExampleApp extends StatelessWidget {
      StatefulShellRouteExampleApp({super.key});
    
      final GoRouter _router = GoRouter(
        initialLocation: '/login',
        routes: <RouteBase>[
          GoRoute(
            path: '/login',
            builder: (BuildContext context, GoRouterState state) {
              return const LoginScreen();
            },
            routes: <RouteBase>[
              GoRoute(
                path: 'detailLogin',
                builder: (BuildContext context, GoRouterState state) {
                  return const DetailLoginScreen();
                },
              ),
            ],
          ),
          StatefulShellRoute.indexedStack(
            builder: (BuildContext context, GoRouterState state,
                StatefulNavigationShell navigationShell) {
              return ScaffoldBottomNavigationBar(
                navigationShell: navigationShell,
              );
            },
            branches: <StatefulShellBranch>[
              StatefulShellBranch(
                routes: <RouteBase>[
                  GoRoute(
                    path: '/sectionA',
                    builder: (BuildContext context, GoRouterState state) {
                      return const RootScreen(
                        label: 'Section A',
                        detailsPath: '/sectionA/details',
                      );
                    },
                    routes: <RouteBase>[
                      GoRoute(
                        path: 'details',
                        builder: (BuildContext context, GoRouterState state) {
                          return const DetailsScreen(label: 'A');
                        },
                      ),
                    ],
                  ),
                ],
              ),
              StatefulShellBranch(
                routes: <RouteBase>[
                  GoRoute(
                    path: '/sectionB',
                    builder: (BuildContext context, GoRouterState state) {
                      return const RootScreen(
                        label: 'Section B',
                        detailsPath: '/sectionB/details',
                      );
                    },
                    routes: <RouteBase>[
                      GoRoute(
                        path: 'details',
                        builder: (BuildContext context, GoRouterState state) {
                          return const DetailsScreen(label: 'B');
                        },
                      ),
                    ],
                  ),
                ],
              ),
            ],
          ),
        ],
      );
    
      @override
      Widget build(BuildContext context) {
        return MaterialApp.router(
          title: 'Go_router Complex Demo',
          theme: ThemeData(
            primarySwatch: Colors.orange,
          ),
          routerConfig: _router,
        );
      }
    }
    
    class RootScreen extends StatelessWidget {
      const RootScreen({
        required this.label,
        required this.detailsPath,
        super.key,
      });
    
      final String label;
      final String detailsPath;
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Root of section $label'),
          ),
          body: Center(
            child: Column(
              mainAxisSize: MainAxisSize.min,
              children: <Widget>[
                Text(
                  'Screen $label',
                  style: Theme.of(context).textTheme.titleLarge,
                ),
                const Padding(padding: EdgeInsets.all(4)),
                TextButton(
                  onPressed: () {
                    GoRouter.of(context).go(detailsPath);
                  },
                  child: const Text('View details'),
                ),
                const Padding(padding: EdgeInsets.all(4)),
                TextButton(
                  onPressed: () {
                    GoRouter.of(context).go('/login');
                  },
                  child: const Text('Logout'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class DetailsScreen extends StatefulWidget {
      const DetailsScreen({
        required this.label,
        super.key,
      });
    
      final String label;
    
      @override
      State<StatefulWidget> createState() => DetailsScreenState();
    }
    
    class DetailsScreenState extends State<DetailsScreen> {
      int _counter = 0;
    
      @override
      void dispose() {
        super.dispose();
      }
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(
            title: Text('Details Screen - ${widget.label}'),
          ),
          body: _build(context),
        );
      }
    
      Widget _build(BuildContext context) {
        return Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Text(
                'Details for ${widget.label} - Counter: $_counter',
                style: Theme.of(context).textTheme.titleLarge,
              ),
              const Padding(padding: EdgeInsets.all(4)),
              TextButton(
                onPressed: () {
                  setState(() {
                    _counter++;
                  });
                },
                child: const Text('Increment counter'),
              ),
              const Padding(padding: EdgeInsets.all(8)),
              const Padding(padding: EdgeInsets.all(4)),
              TextButton(
                onPressed: () {
                  GoRouter.of(context).go('/login');
                },
                child: const Text('Logout'),
              ),
            ],
          ),
        );
      }
    }
    
    class LoginScreen extends StatelessWidget {
      const LoginScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Login Screen')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                ElevatedButton(
                  onPressed: () {
                    context.go('/login/detailLogin');
                  },
                  child: const Text('Go to the Details Login screen'),
                ),
              ],
            ),
          ),
        );
      }
    }
    
    class DetailLoginScreen extends StatelessWidget {
      const DetailLoginScreen({super.key});
    
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: const Text('Details Login Screen')),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <ElevatedButton>[
                ElevatedButton(
                  onPressed: () {
                    // context.go('/sectionA');
                    context.go('/sectionB');
                  },
                  child: const Text('Go to BottomNavBar'),
                ),
              ],
            ),
          ),
        );
      }
    }