4

I am trying to implement a NavigationBar using the new Material You API.

https://api.flutter.dev/flutter/material/NavigationBar-class.html

I was just curious to know if we can implement the same using the Go_Router package .

Abhishek
  • 43
  • 1
  • 6

3 Answers3

6

Updated Answer (v6.0.0)

My original answer was created using GoRouter v3 and it was not possible at the time to keep the NavigationBar in sub screens.

Currently, in version 6, GoRouter allows that using the ShellRoute, where you can use the builder attribute to build a Scaffold with the navigation bar.

You can see the oficial live example here.

I am rewriting the outdated answer below using the GoRouter v6.0.0, and I am leaving the original answer in case someone finds it useful.

Updated Code

  1. We need to create some basic models to store data:
/// Just a generic model that will be used to present some data on the screen.
class Person {
  final String id;
  final String name;

  Person({required this.id, required this.name});
}

/// Family will be the model that represents our tabs. We use the properties `icon` and `name` in the `NavigationBar`.
class Family {
  final String id;
  final String name;
  final List<Person> people;
  final Icon icon;

  Family({
    required this.id,
    required this.name,
    required this.people,
    required this.icon,
  });
}

/// Families will be used to store the tabs to be easily accessed anywhere. In a real application you would use something fancier.
class Families {
  static const List<Icon> icons = [
    Icon(Icons.looks_one),
    Icon(Icons.looks_two),
    Icon(Icons.looks_3)
  ];

  static final List<Family> data = List.generate(
    3,
    (fid) => Family(
      id: '$fid',
      name: 'Family $fid',
      people: List.generate(
        10,
        (id) => Person(id: '$id', name: 'Family $fid Person $id'),
      ),
      icon: icons[fid],
    ),
  );
}
  1. Now we'll create the basic views that will render the model's data:
/// Used to present Person's data.
class PersonView extends StatelessWidget {
  const PersonView({required this.person, Key? key}) : super(key: key);
  final Person person;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text(person.name),
      ),
    );
  }
}

/// This is the view that will be used by each application's tab.
class FamilyView extends StatelessWidget {
  const FamilyView({super.key, required this.family});
  final Family family;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(family.name),
      ),
      body: ListView(
        children: [
          for (final p in family.people)
            ListTile(
              title: Text(p.name),
              onTap: () => context.go('/family/${family.id}/person/${p.id}'),
            ),
        ],
      ),
    );
  }
}
  1. Now, let's finally create the widget that will show the NavigationBar:
/// Widget responsible to render the actual page and the navigation bar.
class ShellScreen extends StatelessWidget {
  final Widget child;
  final int index;

  const ShellScreen({super.key, required this.child, required this.index});

  @override
  Widget build(BuildContext context) {
    if (index < 0 || index >= Families.data.length) {
      // Just in case someone tries to pass an invalid index in the url.
      GoRouter.of(context).go('/');
      return const SizedBox.shrink();
    }
    return Scaffold(
      body: child,
      bottomNavigationBar: NavigationBar(
        destinations: [
          for (final f in Families.data)
            NavigationDestination(
              icon: f.icon,
              label: f.name,
            )
        ],
        onDestinationSelected: (index) => context.go(
          '/family/${Families.data[index].id}',
        ),
        selectedIndex: index,
      ),
    );
  }
}
  1. Finally, this will only work if we define the app's routes using the StackRouter and set the GoRouter as the app's navigator:
void main() {
  usePathUrlStrategy();
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routeInformationProvider: router.routeInformationProvider,
      routeInformationParser: router.routeInformationParser,
      routerDelegate: router.routerDelegate,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      redirect: (_, __) => '/family/${Families.data[0].id}',
    ),
    ShellRoute(
      // The ShellRoute is what make it possible to wrap the subroutes in a common widget using the `builder`
      builder: (BuildContext context, GoRouterState state, Widget child) {
        int index = int.tryParse(state.params['fid'] ?? '0') ?? 0;
        return ShellScreen(index: index, child: child);
      },
      routes: [
        GoRoute(
          path: '/family/:fid',
          builder: (context, state) {
            final fid = state.params['fid']!;
            final family = Families.data.firstWhere((f) => f.id == fid,
                orElse: () => throw Exception('family not found: $fid'));
            return FamilyView(
              key: state.pageKey,
              family: family,
            );
          },
          routes: [
            GoRoute(
              path: 'person/:id',
              builder: (context, state) {
                final fid = state.params['fid']!;
                final id = state.params['id'];

                final person = Families.data
                    .firstWhere((f) => f.id == fid,
                        orElse: () => throw Exception('family not found: $fid'))
                    .people
                    .firstWhere(
                      ((p) => p.id == id),
                      orElse: () => throw Exception('person not found: $id'),
                    );
                return PersonView(key: state.pageKey, person: person);
              },
            ),
          ],
        ),
      ],
    ),
  ],
);

The important part that solves our need is the ShellRouter. It is a route used to display any matching sub-routes, instead of placing them on the root Navigator.

The widget built by the matching sub-route becomes the child parameter of the builder. So, the ShellScreen can render the sub-route widget presenting the navigation bar.

With all these steps you will have this:

enter image description here


Outdated Answer (v3.0.0)

Yes, it is possible [as a matter of fact, it was not possible, but I didn't understand the question at the time].

Let's use the example in GoRouter documentation as a starting point.

  1. We need to create some basic models to store data:
/// Just a generic model that will be used to present some data on the screen.
class Person {
  final String id;
  final String name;

  Person({required this.id, required this.name});
}

/// Family will be the model that represents our tabs. We use the properties `icon` and `name` in the `NavigationBar`.
class Family {
  final String id;
  final String name;
  final List<Person> people;
  final Icon icon;

  Family({
    required this.id,
    required this.name,
    required this.people,
    required this.icon,
  });
}

/// Families will be used to store the tabs to be easily accessed anywhere. In a real application you would use something fancier.
class Families {
  static const List<Icon> icons = [
    Icon(Icons.looks_one),
    Icon(Icons.looks_two),
    Icon(Icons.looks_3)
  ];

  static final List<Family> data = List.generate(
    3,
    (fid) => Family(
      id: '$fid',
      name: 'Family $fid',
      people: List.generate(
        10,
        (id) => Person(id: '$id', name: 'Family $fid Person $id'),
      ),
      icon: icons[fid],
    ),
  );
}
  1. Now we'll create the basic views that will render the model's data:
/// Used to present Person's data.
class PersonView extends StatelessWidget {
  const PersonView({required this.person, Key? key}) : super(key: key);
  final Person person;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: Center(
        child: Text(person.name),
      ),
    );
  }
}

/// This is the view that will be used by each application's tab.
class FamilyView extends StatefulWidget {
  const FamilyView({required this.family, Key? key}) : super(key: key);
  final Family family;

  @override
  State<FamilyView> createState() => _FamilyViewState();
}


class _FamilyViewState extends State<FamilyView>
    with AutomaticKeepAliveClientMixin {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return ListView(
      children: [
        for (final p in widget.family.people)
          ListTile(
            title: Text(p.name),
            onTap: () =>
                context.go('/family/${widget.family.id}/person/${p.id}'),
          ),
      ],
    );
  }
}

  1. Until now we did nothing different compared to the GoRouter documentation, so let's finally create the widget that will show the NavigationBar:
class FamilyTabsScreen extends StatefulWidget {
  final int index;
  FamilyTabsScreen({required Family currentFamily, Key? key})
      : index = Families.data.indexWhere((f) => f.id == currentFamily.id),
        super(key: key) {
    assert(index != -1);
  }

  @override
  _FamilyTabsScreenState createState() => _FamilyTabsScreenState();
}

class _FamilyTabsScreenState extends State<FamilyTabsScreen>
    with TickerProviderStateMixin {
  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(
          title: Text(_title(context)),
        ),
        body: FamilyView(family: Families.data[widget.index]),
        bottomNavigationBar: NavigationBar(
          destinations: [
            for (final f in Families.data)
              NavigationDestination(
                icon: f.icon,
                label: f.name,
              )
          ],
          onDestinationSelected: (index) => _tap(context, index),
          selectedIndex: widget.index,
        ),
      );

  void _tap(BuildContext context, int index) =>
      context.go('/family/${Families.data[index].id}');

  String _title(BuildContext context) =>
      (context as Element).findAncestorWidgetOfExactType<MaterialApp>()!.title;
}

This is the important part of the code above:

/// [...] suppressed code
bottomNavigationBar: NavigationBar(
  destinations: [
    for (final f in Families.data)
      NavigationDestination(
        icon: f.icon,
        label: f.name,
      )
  ],
  onDestinationSelected: (index) => _tap(context, index),
  selectedIndex: widget.index,
),
/// [...] suppressed code

So, basically we are using the NavigationBar almost exactly as we would use the TabBarView.

  1. Finally, this will only work if we define the app's routes and set the GoRouter as the app's navigator:

void main() {
  GoRouter.setUrlPathStrategy(UrlPathStrategy.path);
  runApp(const MyApp());
}

final _router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      redirect: (_) => '/family/${Families.data[0].id}',
    ),
    GoRoute(
        path: '/family/:fid',
        builder: (context, state) {
          final fid = state.params['fid']!;
          final family = Families.data.firstWhere((f) => f.id == fid,
              orElse: () => throw Exception('family not found: $fid'));

          return FamilyTabsScreen(key: state.pageKey, currentFamily: family);
        },
        routes: [
          GoRoute(
            path: 'person/:id',
            builder: (context, state) {
              final fid = state.params['fid']!;
              final id = state.params['id'];

              final person = Families.data
                  .firstWhere((f) => f.id == fid,
                      orElse: () => throw Exception('family not found: $fid'))
                  .people
                  .firstWhere(
                    ((p) => p.id == id),
                    orElse: () => throw Exception('person not found: $id'),
                  );

              return PersonView(key: state.pageKey, person: person);
            },
          ),
        ]),
  ],
);

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      title: 'Flutter Demo',
      routeInformationParser: _router.routeInformationParser,
      routerDelegate: _router.routerDelegate,
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
    );
  }
}

With all these steps you will have this:

enter image description here

Eduardo Vital
  • 1,319
  • 8
  • 15
  • 1
    Thanks for the above but i would like further clarify if its possible to persist the bottom navigation bar after navigating to the new page. – Abhishek Feb 07 '22 at 10:06
  • What do you mean by persisting? Does it mean keep showing the tab bar on every page? Or does it mean keeping the state of the screen (scrolling position by example) when you go back from the new page to the tab bar screen? – Eduardo Vital Feb 07 '22 at 13:46
  • 1
    Sorry if i was not clear. I meant to keep showing the tab bar on every page after navigation. – Abhishek Feb 07 '22 at 18:24
  • 2
    I really wish I could decode this. It's what I need, but I need it simpler. – tazboy Jun 25 '22 at 20:19
2

For anyone searching on a persistent BottomNavBar across all pages, this is actively being discussed on Github,

https://github.com/flutter/packages/pull/2453

Jesse
  • 21
  • 1
1

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 has following features:

  1. Active icon navbar
  2. Persists navBar item's focus when transisted to new page
  3. back button in the transisted page

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