I have already written a post about bottom navigation bar architecture in Flutter. Since then I have changed my toolbox and updated this architecture. These days I mostly use flutter_bloc package for state management (instead of Provider) and auto_route for navigation.

auto_route is a code generator for Flutter. You list all routes in a single place and it will then generate all useful things like route generator and argument classes. You can then use named routes without all the hassle.

This post will guide you how to setup your own bottom navigation with auto_route and flutter_bloc. I will omit descriptions of things that have not changed since the last post. Refer there for more details.

You can check the example app for this post on my GitHub repository.

I used BLoC instead of a ChangeNotifier with Provider. To simplify this example both state and event is a plain int type that is the current index.

navigation_bloc.dart
class NavigationBloc extends Bloc<int, int> {
  NavigationBloc() : super(NavigationTabs.first);

  @override
  Stream<int> mapEventToState(int event) async* {
    yield event;
  }

  final tabs = const <NavigationTab>[
    NavigationTab(
      name: 'First',
      icon: Icon(Icons.home),
      initialRoute: Routes.firstScreen,
    ),
    NavigationTab(
      name: 'Second',
      icon: Icon(Icons.account_circle_rounded),
      initialRoute: Routes.secondScreen,
    ),
    NavigationTab(
      name: 'Third',
      icon: Icon(Icons.settings),
      initialRoute: Routes.thirdScreen,
    ),
  ];

  Future<bool> onWillPop() async {
    // ...
  }
}

Instead of using top-level constants for each tab index I use static class. This is similar to the Icons class in Flutter and should be more familiar.

navigation_bloc.dart
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 first = 0;
  static const second = 1;
  static const third = 3;
}

Each tab is declared as a NavigationTab class. Since my last post this class is stripped down for simplicity. Important is the name field which is used to name each nested ExtendedNavigator.

tab.dart
@immutable
class NavigationTab {
  const NavigationTab({
    this.name,
    this.icon,
    this.initialRoute,
  });

  final String name;
  final Widget icon;
  final String initialRoute;
}

Routers

auto_route example repo for parallel navigation suggests creating router class for each tab. From my experience this complicates things and brings redundancy. I use a single router and then reuse it while using a different initialRoute.

router.dart
@MaterialAutoRouter(
  routes: [
    MaterialRoute<void>(page: Root),
    MaterialRoute<void>(page: FirstScreen),
    MaterialRoute<void>(page: SecondScreen),
    MaterialRoute<void>(page: ThirdScreen),
    MaterialRoute<void>(page: PushedScreen),
  ],
)
class $AppRouter {}

Don’t forget to run build_runner to generate all files. You can use my package build_runner_helper if you don’t want to type this command every time.1

Terminal
flutter packages pub run build_runner build --delete-conflicting-outputs

Declare the root ExtendedNavigator in your MaterialApp. Don’t forget to fill the initialRoute.

main.dart
class ExampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => NavigationBloc(),
      child: MaterialApp(
        // ...
        builder: ExtendedNavigator.builder(
          router: AppRouter(),
          initialRoute: Routes.root,
        ),
      ),
    );
  }
}

Root Widget

Root Widget is now wrapped inside BlocBuilder and uses ExtendedNavigator instead of plain Navigator.

root.dart
class Root extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final bloc = context.bloc<NavigationBloc>();

    return BlocBuilder<NavigationBloc, int>(
      cubit: bloc,
      builder: (context, state) {
        return WillPopScope(
          onWillPop: bloc.onWillPop,
          child: Scaffold(
            body: IndexedStack(
              index: state,
              children: List.generate(bloc.tabs.length, (index) {
                final tab = bloc.tabs[index];

                return TickerMode(
                  // ...
                  child: Offstage(
                    // ...
                    child: ExtendedNavigator(
                      initialRoute: tab.initialRoute,
                      name: tab.name,
                      router: AppRouter(),
                    ),
                  ),
                );
              }),
            ),
            bottomNavigationBar: BottomNavigationBar(
              // ...
            ),
          ),
        );
      },
    );
  }
}

  1. You obviously can put an alias into your .zshrc file if you have a UNIX-like system. I tend to frequently switch between Mac and Windows and managing each platform tends to get annoying. ↩︎