Routing in Flutter is a vast topic as it can be executed in many different ways. Having a logical and simple to navigate routing setup will directly translate into a better user experience. It will also make the code a lot more maintainable for the developers. 

Configuring routing in Flutter, specifically with Navigator 2.0 can be very tedious and time consuming. This is where AutoRoute comes in with its intuitive API and handy code generation that will save you lots of time and effort.

In this lesson you’ll learn how to leverage the simplicity of the AutoRoute and Salomon Bottom Bar packages to create an elegant bottom navigation bar configured with nested routing.

The Finished App

In this tutorial we are going to build a simple app that will have three main sections.

  • Posts
  • Users
  • Settings

The posts section will display some mock post tiles. When you tap on a post tile, you will be taken to the corresponding post page. The users section will have some mock user avatars. When you tap on a user avatar you will be taken to the corresponding user profile page. Lastly, the settings section will only have one page displaying some mock user account information.

These three top-level sections can be navigated through using a minimalistic, customizable bottom navigation bar. Every section is essentially a separate router located within the root router. The posts and users routers have children routes through which you can navigate to individual post pages and user profile pages.

In this tutorial we are going to learn the absolutely simplest way to configure this kind of setup.

Getting Started

Flutter 2.5 Update

On September 8th, 2021, Google's Flutter team announced the release of Flutter 2.5 and Dart 2.14. In this lesson we'll be developing with the updated versions. The provided starter and finished project files are built with the new versions of Dart and Flutter. So, if you haven’t upgraded yet, make sure you go and run the flutter upgrade command in your terminal before continuing with this tutorial.

If for any reason you aren’t ready to upgrade just yet, then don’t worry. You can still follow along with the lesson with some minor adjustments.

  • With the new Flutter version, anytime you create a new project, you'll have the flutter_lints dev dependency included. This will help you write cleaner code right out of the box. If you haven’t upgraded yet, you won’t notice any significant difference in the tutorial other than the flutter_lints dev dependency in the pubspec.yaml file.
  • Some of the dependencies we'll use in this project depend on meta 1.7.0 and if you haven’t upgraded you'll be faced with an issue since the previous version of Flutter is constrained to an earlier version meta. You can easily overcome this by overriding the meta version. Just add the following code to your pubspec.yaml file:

pubspec.yaml

dependency_overrides:
  meta: ^1.7.0
  • If you want to follow along with the tutorial using the starter project files or view the finished project on your device and you haven’t upgraded you should do the following:
    1. Download the project files from GitHub (links provided below).
    2. Create a new Flutter project on your computer. 
    3. Copy the lib folder from the downloaded project and paste it in place of the lib folder in your newly created project. 
    4. Fix the imports.
    5. Make sure you override the meta dependency as mentioned above. 

Dependencies

In this tutorial we'll be using several dependencies and dev dependencies.

For routing, we'll use the auto_route dependency and auto_route_generator dev dependency. Both will be version 2.3.2. The auto route generator will help us generate code that we would otherwise have to write ourselves. This is what’s so great about AutoRoute, it allows us to bypass writing a lot of boilerplate code. To generate the code, we also need to add build_runner version 2.1.2 as a dev dependency.

To create the stylish bottom navigation bar, we'll use the Salomon Bottom Bar package. This package was inspired by the design created by Aurélien Salomon. I chose this package because the design is very clean and appealing and the implementation of this nav bar is incredibly easy. If you’ve ever created a BottomNavigationBar widget in Flutter, then you will already know how to set up the nav bar from the Salomon Bottom Bar package. Their syntaxes are nearly identical. For this project we'll use version 3.1.0 of the package.

Go ahead and add all of these dependencies now. Your pubspec.yaml file should look something like this after you're done:

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  auto_route: ^2.3.2
  salomon_bottom_bar: ^3.1.0
  cupertino_icons: ^1.0.2

dev_dependencies:
  auto_route_generator: ^2.3.2
  build_runner: ^2.1.2
If you’re using Visual Studio Code you can now add dependencies using the command palette. With the newly updated VS Code Flutter plugin, you can simply bring up the command palette and use the “Dart: Add Dependency” and the “Dart: Add Dev Dependency” commands to add packages to your projects.

Starter Project Overview

In order to free ourselves from worries of building the majority of the UI, we'll be working with a starter project in this tutorial. To follow along you can grab the starter and finished projects from the GitHub links below.

In the main.dart file we have an AppWidget which returns a MaterialApp. Right now the MaterialApp has the PostsPage widget as the home argument, but this will change once we implement the routing.

Then, in the lib folder we also have the widgets.dart file which has the PostTile and UserAvatar widgets. This is a necessary separation to declutter the pages where these widgets are used. 

Then, we have a data folder with an app_data.dart file inside of it. This file contains Post and User classes. These classes are used to create mock data for the posts and users in the app.

Please note, that the way mock data is passed around in the app is not based on best practices. It's done in a simplified way so we can focus on the routing implementation.

The remaining project files are separated into folders based on app features. There are quite a few files here already, and since we'll be adding more, this structure will help keep things organized.

Posts folder:

In the posts folder we have the posts_page.dart and the single_post_page.dart files.

  • In the posts_page.dart file we have a StatelessWidget which will display a Column with three PostTile widgets. You may notice that this page doesn’t have a Scaffold. When we get to configuring the bottom navigation bar, you'll see why. 
  • The single_post_page.dart contains a StatelessWidget which will dynamically display a page with a post name and post color corresponding to the PostTile that was tapped from the posts page.

Users folder:

In the users folder we have the user_page.dart and user_profile_page.dart files.

  • The users_page.dart contains a StatelessWidget which displays a Column with three UserAvatar widgets. 
  • In the user_profile_page.dart file we have a StatelessWidget that will display a page with a dynamically set background color and username corresponding to the UserAvatar that was tapped from the users page.

Settings folder:

In the settings folder there is only one file - settings_page.dart. This is the simplest out of all the feature files we covered. The stateless SettingsPage widget located here simply displays a text title and some fake user account data.

Now that you have a solid understanding of the starter project, let’s begin implementing the routing. 

Nested Routing Configuration

In this section we are going to configure a file that will provide the blueprint for the generated routing code. If you worked with AutoRoute in the recent past then the syntax here should look pretty familiar. Keep in mind that this configuration will differ from the standard AutoRouter routing setup. That is because we'll follow the specific guidelines for creating a bottom navigation with nested routing.

Initial Route - HomePage

Before we configure the routing, let’s first make sure we have all of the files necessary to do that. Go ahead and create a new file in the lib folder and call it home_page.dart. This will be the file where we define the bottom navigation bar. For now, just create a StatelessWidget here. It doesn’t matter what it returns, because we'll change this in a short while. You can just have it return a Container for now.

home_page.dart

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

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

AutoRoute Configuration - router.dart

Now, create a new folder in the lib folder and name it "routes". In this folder create a new file and name it router.dart. This is the file where we'll create the blueprint for the code generator.

Let’s begin by setting up the router with an initial HomePage route. Don’t forget to import the auto route package and the home_page.dart file.

router.dart

@MaterialAutoRouter(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    AutoRoute(
      path: '/',
      page: HomePage,
    )
  ],
)
class $AppRouter {}

The syntax here might look a bit unusual, but this is what’s required for the code generation to work correctly.

Here we are specifying the replaceInRouteName argument in order to make our route names less redundant. When you navigate from one page to the next, you'll need to use the generated route names. If you don’t specify the replaceInRouteName the way we did here, the route name for our SinglePostPage would be SinglePostPageRoute. With replaceInRouteName configured, the generated route name in this example would instead be SinglePostRoute.

Then we provide a List of AutoRoute objects as the routes argument. Here we’ve got one AutoRoute object which sets our HomePage as the initial route by providing “/“ as the path argument.

Next, we need to create routers for the app’s posts, users and settings sections. To do this, add a List of AutoRoute objects as the children argument of the existing AutoRoute object, like demonstrated below. Here you’ll also need to import all of the page widget files used.

router.dart

    
...
AutoRoute(
      path: '/',
      page: HomePage,
      children: [
        AutoRoute(
          path: 'posts',
          name: 'PostsRouter',
          page: EmptyRouterPage,
          children: [
            AutoRoute(path: '', page: PostsPage),
            AutoRoute(path: ':postId', page: SinglePostPage),
          ],
        ),
        AutoRoute(
          path: 'users',
          name: 'UsersRouter',
          page: EmptyRouterPage,
          children: [
            AutoRoute(path: '', page: UsersPage),
            AutoRoute(path: ':userId', page: UserProfilePage),
          ],
        ),
        AutoRoute(
          path: 'settings',
          name: 'SettingsRouter',
          page: SettingsPage,
        )
      ],
    )
...

Now, let’s dissect what exactly we are doing here.

Posts and Users Routers:

The posts and users routers are set up in the same manner, so let’s discuss all of the components present in their configuration now.

  • path: For the path argument we provide the path name we'd like the router to have.
  • name: The String provided as the name argument will be used to generate the name for the router. This name can be used to access the router to configure the bottom navigation bar, or navigate between pages located in different routers/navigation tabs.
  • page: Here we are providing EmptyRouterPage (provided by the AutoRoute package) as the page argument. You should do this whenever you have nested routes for a specific bottom navigation tab.
  • children: This children argument accepts aList of AutoRoute objects. This will be the List of nested routes that will live under the given router. The first route has an empty String for the path argument. This indicates that this will be the first page to be displayed when you select the corresponding navigation tab. The second AutoRoute object path looks a bit different. The ':postId' and ':userId' syntax is used to create dynamic segments. With this setup, if you are running your app in the browser and enter something like “/posts/1” you'll be taken to the page for the post with the postId field equal to 1. For this to work correctly, you also need to annotate the postId and userId constructor parameters in the page files. We'll do this soon.

Settings:

In the settings router the main differences are that there are no children and the page argument is set to SettingsPage instead of EmptyRouterPage. This is because we don't have any nested routes here, and in this case we should just set the page argument to the page we want displayed.

Before creating the generated code file, let’s do one more thing. As mentioned earlier, in order for the dynamic segments defined as ':postId' and ':userId' to work we need to head over to the single_post_page.dart and user_profile_page.dart files and annotate the corresponding constructor parameters with @PathParam('optional-alias'). If you define an alias, it should match the segment name you defined in the router.dart file. If your field name matches the segment name, then you don't need to provide an alias. Go ahead and do this for SinglePostPage first.

single_post_page.dart

const SinglePostPage({
  Key? key,
  @PathParam() required this.postId,
}) : super(key: key);

Since our postId  field name matches the segment name defined in the router.dart file we didn't include the alias in the annotation. Now you can do the same thing for the UserProfilePage.

user_profile_page.dart

const UserProfilePage({
  Key? key,
  @PathParam() required this.userId,
}) : super(key: key);

To create a file using code generation from the blueprint we created, run the following terminal command.

👩‍💻terminal

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

We are using the build flag which will cause the generator to run only once. If instead you anticipate making several changes to your router.dart file, then you can swap the build flag for watch. Using watch will run the generator anytime you make changes.

Now you should see a router.gr.dart file inside of the routes folder. If you open it up you can see how many lines of code this helpful generation tool saved us from writing.

Linking the Router to the App

Now that we’ve configured the router, we can connect it to our app. Head over to the main.dart file and over there we need to change a few things. Right now the AppWidget returns a MaterialApp. We need to swap it for MaterialApp.router. You can go ahead, delete all of the code inside of the AppWidget build method, and configure your main.dart file in the following way.

main.dart

void main() => runApp(AppWidget());

class AppWidget extends StatelessWidget {
  AppWidget({Key? key}) : super(key: key);
  final _appRouter = AppRouter();
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      debugShowCheckedModeBanner: false,
      title: 'Bottom Nav Bar with Nested Routing',
      routerDelegate: _appRouter.delegate(),
      routeInformationParser: _appRouter.defaultRouteParser(),
    );
  }
}

Now, let’s discuss what we’ve got here. First, we initialized the AppRouter and stored it inside an _appRouter variable. Because of that we also had to remove the const next to the AppWidget in the runApp and in the constructor. The AppRouter is generated by AutoRoute for us, so make sure to import the router.gr.dart file here. By initializing the AutoRoute inside of the root widget, we make this router accessible across the entire app throughout the app’s lifecycle.

Then we provided two mandatory, routing specific arguments to the MaterialApp.router. The values we provided for the routerDelegate and routeInformaitonParser arguments come from the generated AppRouter object.

That’s it, we now have all of the necessary configurations in place. Next, we are going to start implementing the bottom navigation.

Implementing the Bottom Navigation

AutoTabsScaffold

We can finally start implementing the bottom nav bar for the app. Luckily, the AutoRoute package has a helpful widget which makes it incredibly simple to configure this. Open the home_page.dart file we created earlier and replace the Container in the build method with an AutoTabsScaffold widget. This widget comes from the AutoRoute package, so make sure to import it here.

home_page.dart

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

  @override
  Widget build(BuildContext context) {
    return AutoTabsScaffold();
  }
}

The AutoTabsScaffold widget allows us to easily create a Scaffold with tabbed routing. This Scaffold will persist throughout the app. We want our app to have an app bar and a bottom navigation bar.

If you take a look, you may notice that unlike the PostsPage,
the SinglePostPage has its own Scaffold. This is because we want the PostsPage to have the Scaffold defined through the AutoTabsScaffold widget. In the SinglePostPage and the UserProfilePage however, we defined a separate Scaffold to be able to specify a custom background color.

First, let’s create an app bar. For this, we can use the appBarBuilder callback that will return an AppBar widget.

home_page.dart


...
return AutoTabsScaffold(
  appBarBuilder: (_, tabsRouter) => AppBar(
    backgroundColor: Colors.indigo,
    title: const Text('FlutterBottomNav'),
    centerTitle: true,
    leading: const AutoBackButton(),
  ),
);
...

The appBarBuilder callback gives us access to the context and a TabsRouter object. We won’t use them for this app bar, though. Our app bar has a custom background color, centered title, and an AutoBackButton as the leading argument. The AutoBackButton is a widget provided by the AutoRoute package to easily handle nested router popping. We will see it in action shortly.

Next, let’s give our AutoTabsScaffold a custom background color and specify the routers we want included in the bottom navigation bar. To do this we will provide a list of routers we created earlier to the routes argument in the order we want the corresponding navigation tabs to be displayed.

home_page.dart


...
backgroundColor: Colors.indigo,
routes: const [
  PostsRouter(),
  UsersRouter(),
  SettingsRouter(),
],
...

Now we can configure the bottom navigation bar itself. For this we will use the bottomNavigationBuilder argument.

home_page.dart


...
bottomNavigationBuilder: (_, tabsRouter) {},
...

This callback gives us access to the context and a TabsRouter object. We will need to use the TabsRouter object here. You can use this callback to return the BottomNavigationBar widget that is included with Flutter, but you can also return a custom navigation bar as well. To demonstrate this, we will use the Salomon Bottom Bar package to create the bottom nav.

Salomon Bottom Bar

If you’ve ever used the BottomNavigationBar widget that ships with Flutter, then you will find the SalomonBottomBar incredibly intuitive to configure. First, we need to import the Salomon Bottom Bar package into the home_page.dart file. Then we need to return the SolomonBottomBar widget from the bottomNavigationBuilder callback. Once this is in place, we need to specify the following arguments for the SalomonBottomBar widget:

  • margin: creates some spacing around the navigation tabs. This is optional of course.
  • currentIndex: index of the current navigation tab 
  • onTap: a function that returns the index of the tab that was tapped
  • items: a List of SolomonBottomBarItem widgets, one widget for every navigation tab in the bottom nav.

The SolomonBottomBarItems require their own setup. For our app we will provide values for the following arguments:

  • selectedColor
  • icon
  • title

Once all of this is done the SolomonBottomBar widget should resemble the code snippet below.

home_page.dart


...
return SalomonBottomBar(
  margin: const EdgeInsets.symmetric(
    horizontal: 20,
    vertical: 40,
  ),
  currentIndex: tabsRouter.activeIndex,
  onTap: tabsRouter.setActiveIndex,
  items: [
    SalomonBottomBarItem(
      selectedColor: Colors.amberAccent,
      icon: const Icon(
        Icons.post_add,
        size: 30,
      ),
      title: const Text('Posts'),
    ),
    SalomonBottomBarItem(
      selectedColor: Colors.blue[200],
      icon: const Icon(
        Icons.person,
        size: 30,
      ),
      title: const Text('Users'),
    ),
    SalomonBottomBarItem(
      selectedColor: Colors.pinkAccent[100],
      icon: const Icon(
        Icons.settings,
        size: 30,
      ),
      title: const Text('Settings'),
    ),
  ],
);
...

As you can see, this required a minimal amount of work. There are lots of other ways you can customize the bottom nav, but for this simple example we will stick with what we have now.

Now you can run the app and see the bottom navigation bar in action. When you tap on the bottom nav bar tabs, you should be navigated to the corresponding pages. You will also see the sleek navigation bar with animations when tapping on the tabs.

Navigating to SinglePostPage & UserProfilePage

Right now we can comfortably navigate to the PostsPage, UsersPage, and SettingsPage using the bottom nav. What we are missing though, is the ability to navigate to the SinglePostPage and UserProfilePage routes when tapping on the post tiles and user avatars.

AutoRoute provides many different methods for you to be able to navigate around your app. In this example we will stick to the simple push method. To call the push method or any of the other navigation methods, you first need to get the scoped router by calling either AutoRouter.of(context) or context.router. Then you can call the method of your choice on the scoped router and pass the desired route(s) to it. Head over to the posts_page.dart file and set up the routing to the SinglePostRoute by calling the push method in the onTileTap argument.

posts_page.dart


...
child: Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    for (int i = 0; i < posts.length; i++)
      PostTile(
        tileColor: posts[i].color,
        postTitle: posts[i].title,
        onTileTap: () => context.router.push(
          SinglePostRoute(
            postId: posts[i].id,
          ),
        ),
      ),
  ],
),
...

Now, let’s move over to users_page.dart and do the same kind of thing for the UserProfileRoute in the onAvatarTap argument.

users_page.dart


...
child: Column(
  mainAxisAlignment: MainAxisAlignment.center,
  children: [
    for (int i = 0; i < users.length; i++)
      UserAvatar(
        avatarColor: users[i].color,
        username: 'user${users[i].id}',
        onAvatarTap: () => context.router.push(
          UserProfileRoute(
            userId: users[i].id,
          ),
        ),
      ),
  ],
),
...
In this app we are navigating to routes that are located in the same router. If you want to navigate from a page in one navigation tab/router to a page in another navigation tab/router you can do that as well. Suppose you wanted to navigate from a UserProfileRoute to SinglePostRoute, you could do it like this:
context.navigateTo(PostsRouter(children: SinglePostRoute(postId: id))).

Now you can restart the app and try out all the navigation we’ve set up. Note that when tapping on the post tiles and user avatars, you should see a back button appear in the Scaffold. That is because earlier we added an AutoBackButton widget as the leading argument of the AutoTabsScaffold.

Conclusion

That’s all, our app is complete! Just the time we saved by using AutoRouter makes this a no-brainer of an approach when it comes to creating a bottom navigation bar. You should now be able to use what you’ve learned here in your own projects and customize everything for your individual use cases.

About the author 

Ashley Novik

Ashley is a Flutter developer and tutor at Reso Coder with a passion for tech and an infinite drive to learn and teach others 😄. On her days off she enjoys exploring nature and powering through off-road trails on her mountain bike 🚵‍♀️.

You may also like

Flutter SVG Animations With Rive

Flutter SVG Animations With Rive
  • Great tutorial, much thanks.
    One question, How would you go about if you want the bottom bar onTap event to always send you to the root page of the section your in?
    For example, if you’re on a single post page and you press the post item in the bottom nav it will always take you to the posts list page.

    • Hi Abdullah,

      Thanks for the positive feedback!

      I’m not sure if this is the most efficient way to do this, but I managed to achieve the functionality you’re looking for by popping routes until the root route in a specific tab if the tab index you’re selecting is the the same as the index for the currently active tab. Like so:

      onTap: (index) => index == tabsRouter.activeIndex
      ? tabsRouter.stackRouterOfIndex(index)?.popUntilRoot()
      : tabsRouter.setActiveIndex(index),

      Hope this helps!

      • Thanks Ashley!

        popUntilRoot() is exactly what I needed. In my case I want it to go to the root regardless, so i wrote mine like this:

        onTap: (index) {
        tabsRouter.stackRouterOfIndex(index)?.popUntilRoot();
        tabsRouter.setActiveIndex(index);
        },

  • Awesome tutorial, but any idea how to navigate across different router for child widget? For example, if I want to link a specific user page to be accessed through the single post page?

    I’ve tried to do that and it raised an exception:
    Error: [PostsRouter Router] Router can not navigate to UserProfilePage

    • Sorry, I just realized the context.navigateTo part, it did work for me like the following

      “`
      context.navigateTo(UsersRouter(children: UserProfilePage(userId: 1)));
      “`

  • {"email":"Email address invalid","url":"Website address invalid","required":"Required field missing"}
    >