Bottom navigation using auto_route and block packages.

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.

  1. animated_bottom_navigation_bar: ^version
  2. auto_route: ^version
  3. flutter_bloc: ^version
  4. 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,
                ),
              ),
            );
          },
        );
      },
    );
  }
}
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;
}

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);