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 .
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 .
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.
/// 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],
),
);
}
/// 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}'),
),
],
),
);
}
}
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,
),
);
}
}
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:
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.
/// 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],
),
);
}
/// 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}'),
),
],
);
}
}
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
.
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:
For anyone searching on a persistent BottomNavBar across all pages, this is actively being discussed on Github,
ShellRouter
with GoRouter
to create Navigation Bar
Things to keep in mind while using context.go()
from ShellRoute
to GoRoute
parentNavigatorKey
prop in each GoRoute
context.go()
to replace page , context.push()
to push page to stack
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
navbar
navBar
item's focus when transisted to new pageback button
in the transisted page
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"),
),
),
);
},
),
],
);
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);
}