Those snippets provide a bottom navigation bar. The navigation between the different pages is happening with the help of the auto_route package. For providing the navigation component we’re using the bloc package. For the UI we’re using a package called animated_bottom_navigation_bar. There are 5 menu options. The 3rd one though, the middle one, is outside of the navigation bar, as a floating action button.
Dependencies
Add the following packages in the pubspec.yaml first.
- animated_bottom_navigation_bar: ^version
- auto_route: ^version
- flutter_bloc: ^version
- equatable: ^version
and run the pub get command from the terminal.
router.dart
All routes are defined in this single router.
Do NOT specify initial
to true for any of these routes so that we can reuse this router for nested navigation. We will declare initialRoute
in each ExtendedNavigator
accordingly.
@MaterialAutoRouter(routes: [
MaterialRoute<void>(page: Root),
MaterialRoute(page: FeedStateless),
MaterialRoute(page: ProfileScreen),
MaterialRoute(page: MatchesScreen),
MaterialRoute(page: ChatsScreen),
... ... ...
MaterialRoute(page: LoginScreen),
MaterialRoute(page: SplashScreen),
MaterialRoute(page: UnauthorizedHome),
])
class $AppRouter {}
CAUTION After adding, removing or modifying the routes, we need to auto generate the AppRouter. To do so we run from the terminal:
flutter packages pub run build_runner build --delete-conflicting-outputs
main.dart
This will redirect to the splash where we check for the user authentication. If the user is not authenticated it redirects to the login / sign up. Otherwise it will redirect to the root widget, which is responsible for the functionality of the bottom navigation bar.
main() async {
runApp(
MyApp(),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _buildMainChild(context));
}
_buildMainChild(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<NavigationBloc>(create: (context) => NavigationBloc()),
...
],
child: MaterialApp(
builder: ExtendedNavigator(
router: AppRouter(),
initialRoute: Routes.splashScreen,
),
),
);
}
}
root.dart
The widget responsible for the bottom navigation.
class Root extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _root(context);
}
Widget _root(BuildContext context) {
return BlocBuilder<AuthenticationBloc, AuthenticationState>(
cubit: BlocProvider.of<AuthenticationBloc>(context),
builder: (context, state) {
return BlocBuilder<NavigationBloc, int>(
cubit: BlocProvider.of<NavigationBloc>(context),
builder: (context, state) {
return WillPopScope(
onWillPop: BlocProvider.of<NavigationBloc>(context).onWillPop,
child: Scaffold(
extendBody: true,
body: IndexedStack(
index: state,
children: List.generate(
BlocProvider.of<NavigationBloc>(context).tabs.length,
(index) {
final tab =
BlocProvider.of<NavigationBloc>(context).tabs[index];
return TickerMode(
enabled: index == state,
child: Offstage(
offstage: index != state,
child: ExtendedNavigator(
initialRoute: tab.initialRoute,
name: tab.name,
router: AppRouter(),
),
),
);
}),
),
floatingActionButton: FloatingActionButton(
child: Icon(
Icons.home,
color: Colors.black45,
),
backgroundColor: Colors.transparent,
foregroundColor: Colors.transparent,
elevation: 0,
onPressed: () =>
// manually navigate. It looks like it's part of the navigation bar but it's not
BlocProvider.of<NavigationBloc>(context).add(4),
),
floatingActionButtonLocation:
FloatingActionButtonLocation.centerDocked,
bottomNavigationBar: AnimatedBottomNavigationBar(
onTap: BlocProvider.of<NavigationBloc>(context).add,
activeIndex: state,
icons: iconList,
gapLocation: GapLocation.center, // add a gap for the floating action button
notchSmoothness: NotchSmoothness.defaultEdge,
),
),
);
},
);
},
);
}
}
nav_bloc.dart
class NavigationBloc extends Bloc<int, int> {
NavigationBloc() : super(NavigationTabs.home);
@override
Stream<int> mapEventToState(int event) async* {
yield event;
}
final tabs = <NavigationTab>[
NavigationTab(
name: Strings.navChat,
icon: Icon(
Icons.chat_bubble_outline_rounded,
color: Colors.black,
),
initialRoute: Routes.chatsScreen,
),
NavigationTab(
name: Strings.navMatches,
icon: Icon(
Icons.notifications_outlined,
color: Colors.black,
),
initialRoute: Routes.matchesScreen,
),
NavigationTab(
name: Strings.navProfile,
icon: Icon(
Icons.account_circle_rounded,
color: Colors.black,
),
initialRoute: Routes.profileScreen,
),
NavigationTab(
name: Strings.navRecord,
icon: Icon(
Icons.videocam_outlined,
color: Colors.black,
),
initialRoute: Routes.recordQuestion,
),
NavigationTab(
name: '',
icon: Icon(
Icons.home_outlined,
color: Colors.black,
),
initialRoute: Routes.feedStateless,
),
];
/// return true to exit
Future<bool> onWillPop() async {
final currentNavigatorState = ExtendedNavigator.named(tabs[state].name);
print("nav:: ${currentNavigatorState.canPop()} // $state");
if (currentNavigatorState.canPop()) {
currentNavigatorState.pop();
} else {
if (state == NavigationTabs.home) {
return true;
} else {
add(NavigationTabs.home);
}
}
return false;
}
}
class NavigationTabs {
/// Default constructor is private because this class will be only used for
/// static fields and you should not instantiate it.
NavigationTabs._();
static const chat = 0;
static const matches = 1;
static const profile = 2;
static const record = 3;
static const home = 4;
}
Navigation to the rest of the routes
For navigating and having the bottom bar visible:
ExtendedNavigator.of(context).push(Routes.chatScreen);
For navigating and hide the bottom bar (full height screen):
ExtendedNavigator.of(context, rootRouter: true).push(Routes.chatScreen);