Navigation in Flutter with AutoRoute

Andrea Briasco
OverApp
Published in
7 min readMay 29, 2023

--

Introduction

Navigation is an essential aspect of an application because the vast majority of apps have multiple screens to show to the user and to make a great UX (User Experience) the navigation should be well managed and done properly.

In this article I am going to show you how to manage the navigation between a few simple screens, sending some data between them and managing the navigation stack to previous destinations.

To implement and use the navigation in Flutter can be done by using either the Navigator widget which comes without having to install any package, or installing one like AutoRoute.

Using an external package can be useful if you want more control over how routes are defined and managed in your app.

Why AutoRoute?

AutoRoute is based on Navigator 2.0, a declarative API which sets the history stack of the Navigator and has a Router widget to configure the Navigator based on app state and system events.

I chose to install this package for the navigation which has some cool handy features.

It allows for strongly-typed arguments passing, effortless deep-linking and it uses code generation to simplify routes setup, with that being said it requires a minimal amount of code to generate everything needed for navigation inside of your App.

Installation

Before we start implementing the navigation’s logic we need to install the package in the pubspec.yaml file as shown below.

dependencies:                    
auto_route: ^7.1.0

dev_dependencies:
auto_route_generator: ^7.0.0
build_runner: ^2.4.4

After adding the needed navigation dependency, sync your project or type below command in the terminal. Make sure you execute this command inside your project directory.

flutter packages get

Setup

To auto-generate the navigation we first need to create the router.dart file and put it in the lib directory, this is where all our code will be.

In it we create a Router class and annotate it with @AutoRouterConfig then extend $YourClassName and override the routes getter which contains a list of AutoRoute objects, this will wrap all our routes.

@AutoRouterConfig()      
class AppRouter extends $AppRouter {

@override
List<AutoRoute> get routes => [
/// routes go here
];
}

The “$” character on the extended class is needed to tell the library to generate that Router class for the navigation, while the annotation takes care of creating the navigation for each of the pages added in the array.

Create the UI

Now that the dependency setup is done we are going to compose three simple screen widgets in the lib directory.

The first screen represents the start destination and will have a button that takes us to the second screen, this one will have an up back button on the AppBar to navigate us back and a button which directs us to the third screen, the latter will have an up back button too and a button which navigates us to the first screen clearing the stack.

Let’s now put all of this in practice!

  • First Screen
@RoutePage()
class FirstScreen extends StatefulWidget {
const FirstScreen({super.key});

@override
State<StatefulWidget> createState() => _FirstScreenState();
}

class _FirstScreenState extends State<FirstScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("First Screen"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"First Screen",
style: Theme.of(context).textTheme.headlineMedium,
)
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
/// navigation goes here
},
),
);
}
}
  • Second Screen
@RoutePage()
class SecondScreen extends StatefulWidget {
const SecondScreen({super.key});

@override
State<StatefulWidget> createState() => _SecondScreenState();
}

class _SecondScreenState extends State<SecondScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Second Screen"),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
/// navigation goes here
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Second Screen",
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
/// navigation goes here
},
),
);
}
}
  • Third Screen
@RoutePage()
class ThirdScreen extends StatefulWidget {
const ThirdScreen({super.key});

@override
State<StatefulWidget> createState() => _ThirdScreenState();
}

class _ThirdScreenState extends State<ThirdScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text("Third Screen"),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
/// navigation goes here
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Third Screen",
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.restart_alt),
onPressed: () {
/// navigation goes here
},
),
);
}
}

To generate the routes we annotated the widgets pages with @RoutePage() which allows them to be constructed by the router.

As you can see in the code above, in each page, I left the onPressed() block empty for each click events, we will add the code for the navigation in a moment.

Now simply run the generator with the following command which will generate a router.gr.dart file in your directory, this will handle the navigation for us.

flutter packages pub run build_runner watch

Note that in case you end up adding some new screens in your app after generating the file for the navigation, you just need to run the above command again to update that generated file, don’t forget to also declare the added screen with the @RoutePage() annotation.

Finalise the setup

Finally we can add the generated routes to the routes List we created earlier.

@override
final List<AutoRoute> routes = [
AutoRoute(page: FirstRoute.page, path: '/'),
AutoRoute(page: SecondRoute.page),
AutoRoute(page: ThirdRoute.page),
];

Here the “/” character in the path for that route states that the page is used as the first screen by the AutoRoute package library.

The route names are auto-generated by the package, by default it replaces words like “Page” and “Screen” with “Route”.

e.g. FirstScreen would be FirstRoute

Auto generated route names can be a bit long with the Route suffix e.g. ProductDetailsPage would be ProductDetailsPageRoute
You can replace some relative parts in your route names by providing a replacement in the follow pattern whatToReplace,replacement what to replace and the replacement should be separated with a comma , e.g. ‘Page,Route’ so ProductDetailsPage would be ProductDetailsRoute
defaults to ‘Page|Screen,Route’, ignored if a route name is provided.

There are two ways to decide our own names for the route pages.

  • In the actual page, inside the @RoutePage() annotation, add the name attribute and set the name you prefer e.g. @RoutePage(name:“FirstScreen”), this is the easiest way but it could be a bit repetitive when set on each screen page so let’s opt for the second way.
  • In the router.dart file, inside the @AutoRouterConfig() annotation, add the replaceInRouteName attribute and here is where the default value is “Page|Screen,Route”, that means if your pages name have either “Page” or “Screen” in it, this will be replaced with “Route”, so if we want to keep our route name we add the value “Route,Page|Screen” e.g. @AutoRouterConfig(replaceInRouteName: “Route,Page|Screen”).

We now refactor the route class with the pages name we decided, below is the final code.

@AutoRouterConfig(replaceInRouteName: "Route,Page|Screen")
class AppRouter extends $AppRouter {
@override
final List<AutoRoute> routes = [
AutoRoute(page: FirstScreen.page, path: '/'),
AutoRoute(page: SecondScreen.page),
AutoRoute(page: ThirdScreen.page),
];
}

To finalise the setup we need to hook the MaterialApp with the route class because this is where AppRouter is registered and the navigation is set.

@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _appRouter.config(),
title: "Flutter Navigation",
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
);
}

Navigation in action

The navigation is now ready to use so let’s implement the logic for the click events in the onPressed() empty block in each page.

Let’s start the flow from the first screen.

floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
AutoRouter.of(context).push(
SecondRoute(title: "Second Screen"),
);
},
),

As you can see in the code above, to make the navigation in action with the FloatingActionButton we created, we used the AutoRouter object and called the push() method and passed in the screen route destination we want to navigate to.

That method, as the word suggests, pushes the destination on top of the current one, this creates a stack in the navigation.

To notice that we also passed a title as a String argument so that our second screen will receive and use it as the text title for that page.

In the second screen we handle the passed argument in the class constructor, then when we need it we just reference the widget instance to set the Text in the AppBar.

/// ...

final String title;

const SecondScreen({super.key, required this.title});

/// ...

appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
automaticallyImplyLeading: false,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios),
onPressed: () {
AutoRouter.of(context).pop();
},
),
),

/// ...

floatingActionButton: FloatingActionButton(
child: const Icon(Icons.arrow_forward_ios),
onPressed: () {
AutoRouter.of(context).push(
ThirdRoute(title: "Third Screen"),
);
},
),

For the back navigation we used an IconButton in the AppBar and called the pop() method on the AutoRouter object, this handles clearing the navigation stack and navigate us back to the previous screen.
While the forward FloatingActionButton navigates us to the next page.

In the third screen we handle the logic for the up back button as we did before, it takes us to the second page.

floatingActionButton: FloatingActionButton(
child: const Icon(Icons.restart_alt),
onPressed: () {
AutoRouter.of(context).popUntilRoot();
},
),

While on the FloatingActionButton, as you can see above, we used popUntilRoot() which clears all the navigation stack, this takes us to the first page since the stack got cleared out and that one was the very first destination we defined in the list of routes.

Wrapping up

We just arrived at the end of this article, I showed you a quick intro about the navigation in Flutter and how to setup the AutoRoute package library, how to name the route pages as you prefer, we handled argument-passing in navigation and learned how to manage the stack and clearing it completely to make the navigation directing us to the very first screen page.

Thank you for reading my article, I hope it was helpful for you and that you liked it.

Many thanks to my colleague Franceska for designing and drawing the lovely illustration at the top of my article.

Until the next one, Cheers! 😁✌️

--

--