17

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?

krishnaacharyaa
  • 14,953
  • 4
  • 49
  • 88
Florian Leeser
  • 590
  • 1
  • 6
  • 20

4 Answers4

21

Now you can use ShellRouter with GoRouter to create Navigation Bar


Explaination:

Things to keep in mind while using context.go() from ShellRoute to GoRoute

  1. Specify parentNavigatorKey prop in each GoRoute
  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

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
8

This is an outstanding feature request for go_router that I hope to resolve in the coming weeks. Stay tuned.

csells
  • 2,453
  • 1
  • 20
  • 18
  • I saw you closed the feature request, so how would I achieve this goal now, since I can only read about a new Navigation Stack inside of every Tab, which is not what I would like to do. – Florian Leeser Feb 26 '22 at 19:51
  • 1
    The go_router package issues are listed here: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%22p%3A+go_router%22 – csells Feb 27 '22 at 21:55
  • 1
    The issue was moved here: https://github.com/flutter/flutter/issues/99126 Still not much progress after 6 months though :( – Andrey Gordeev Aug 13 '22 at 09:21
4

The solution that worked for me in the end was the following:

I awas able to create a route that specifies which values are allowed:

GoRoute(
  path: '/:screen(home|discover|notifications|profile)',
  builder: (BuildContext context, GoRouterState state) {
    final String screen = state.params['screen']!;

    return TabScreen(screen: screen);
  }
)

With that done, I pass whatever value the route contains (e.g. '/home' or '/discover') to the TabScreen which then displays the exact same Widget, but without reloading also the TabBar over and over again:

class TabScreen extends StatelessWidget {

  const TabScreen({
    super.key,
    required this.screen
  });

  final String screen;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Expanded(child: screen == 'home' ? const HomeScreen() : screen == 'discover' ? const DiscoverScreen() : screen == 'notifications' ? const NotificationsScreen() : ProfileScreen(),
        CustomBottomNavigationBar()
     
      ]
    );
  }
}
Florian Leeser
  • 590
  • 1
  • 6
  • 20
  • 2
    Would you be willing to create a gist to show your CustomBottomNavigationBar() and one of the tabbed screens perhaps DiscoverScreen()? Im having the same issue and im curious to see how you pass the screen value – CharlesAE Jul 01 '22 at 12:18
  • @CharlieNorris https://gist.github.com/fleeser/f9bcf0ef6053a69490db85192c7b9ab6 – Florian Leeser Aug 03 '22 at 11:58
  • Unfortunately, this makes using subroutes very messy. – Andrey Gordeev Aug 13 '22 at 09:33
  • @AndreyGordeev You can define the TabBar routes as I did like "/:screenName(user/book)" and define the first subroute as a complete new route "/user/:uid" and put all the underlying subroutes in there then. But I agree that for now, the solutions are more a work around than a real solution. – Florian Leeser Aug 16 '22 at 11:37
4

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'),
            ),
          ],
        ),
      ),
    );
  }
}
kokemomuke
  • 554
  • 7
  • 10