
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:
- Download the project files from GitHub (links provided below).
- Create a new Flutter project on your computer.
- Copy the lib folder from the downloaded project and paste it in place of the lib folder in your newly created project.
- Fix the imports.
- 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
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.
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 aColumn
with threePostTile
widgets. You may notice that this page doesn’t have aScaffold
. 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 thePostTile
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 aColumn
with threeUserAvatar
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 theUserAvatar
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 thename
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 thepage
argument. You should do this whenever you have nested routes for a specific bottom navigation tab. - children: This
children
argument accepts aList
ofAutoRoute
objects. This will be theList
of nested routes that will live under the given router. The first route has an emptyString
for thepath
argument. This indicates that this will be the first page to be displayed when you select the corresponding navigation tab. The secondAutoRoute
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 thepostId
field equal to 1. For this to work correctly, you also need to annotate thepostId
anduserId
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.
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.
PostsPage
,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
ofSolomonBottomBarItem
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.



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,
),
),
),
],
),
...
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.
the starter project and the finished project are the same, something wrong in the repo or the branches
Hi Kane,
If you want to get to the specific commit (in this case the Starter Project) you can go about this in 2 ways.
1. If you are using your terminal to clone the repo you also need to run the
git checkout
command. The commit id is 66e1a2e for this starter project.
2. If you want to download via a link you can use the link below. This is a straight download link for the zip file, so when you click on it, it will download the file right away.
https://github.com/ResoCoder/flutter-bottom-navigation-with-nested-routing-tutorial/archive/66e1a2e52e43b5aba570e3f5574c0a84dc02c48e.zip
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)));
“`
Base on the example…..Suppose you wanted to navigate from a UserProfileRoute to SinglePostRoute, you could do it like this:
context.navigateTo(PostsRouter(children: SinglePostRoute(postId: id))).
The problem is, when I click the back button on SinglePostRoute, I am redirecting to its parent PostsRouter, not on UserProfileRoute
Any solution to this, you can reproduct the problem on your own example
Hi! Ty! Nice tutorial, but i am having hero animation problems! Doesn’t work with your approach! the animation twitches and runs to the end
[INFO] Generating build script…
[INFO] Generating build script completed, took 481ms
[INFO] Initializing inputs
[INFO] Reading cached asset graph…
[INFO] Reading cached asset graph completed, took 67ms
[INFO] Checking for updates since last build…
[INFO] Checking for updates since last build completed, took 747ms
[INFO] Running build…
[INFO] Running build completed, took 14ms
[INFO] Caching finalized dependency graph…
[INFO] Caching finalized dependency graph completed, took 39ms
[SEVERE] auto_route_generator:autoRouteGenerator on lib/router.dart (cached):
Route must have either a page or a redirect destination
[SEVERE] Failed after 73ms
pub finished with exit code 1
Please provide solution for this
How can I set the currentIndex to 1 (show the item in the middle first)?
Running from the final code…
Running Gradle task ‘assembleDebug’…
../../development/flutter/.pub-cache/hosted/pub.dartlang.org/auto_route-2.3.2/lib/src/router/widgets/auto_router_delegate.dart:43:43: Error: Required named parameter ‘type’ must be provided.
?.routerReportsNewRouteInformation(
^
I had to remove the dependency override in pubspec.yaml and then update all modules to the latest version ‘flutter pub upgrade –major-versions’.
afterwards run the flutter build_runner command again.
How I can achieve following condition:
I want to navigate to Profile section on click of the Post1 card from home page, and also the selected buttom navigation bar will be the profile page
Thank you for the amazing lesson, i followed your instructions yet when i run this command in the terminal I get this error
” Duplicate route names must have the same path! (name: MainRouter, path: auth)
Note: Unless specified, route name is generated from page name.
package:emart/router.dart:72:7
╷
72 │ class $AppRouter {}
│ ^^^^^^^^^^
╵
“
Great tutorial, much thanks.
One question, How could you use AutoRouteObserver to detect tab change. Regards.