How to setup Routing in a Flutter app

Sandro Maglione

Sandro Maglione

Mobile development

One of the most important part of a Flutter app is routing and navigation. We want a powerful routing system that allows to easily navigate between pages.

The routing configuration is usually one of the first steps when creating a new app, and it will remain the same throughout the project. This means that it is crucial to setup a robust setup from the start. Let me show how I achieve this.

Check out also my Environmental Variables in Flutter post. Setting up environmental variables is also one of the foremost steps when creating a new app.


What is routing

An app is made of multiple screens. The user navigates between these screens while using the app. Routing is the mechanism used to manage the screens of an app.

Some of the main actions performed and managed by a routing system are:

  • Show the initial screen when the app first loads
  • Provide a way to open a new screen
  • Manage the action of coming back to the previous screen (for example when clicking the back button)

On top of these core requirements, the developer needs a powerful enough system to organize multiple screens, provide variables to each screen, access each screen in the stack, and more.

How routing is implemented

The data structure behind a routing system is called Stack. A Stack, as the name implies, consists in a series of pages piled on top of each other.

The developer can perform two basic actions:

  • push (add) pages on top of the stack (only on top!)
  • pop (remove) the topmost page from the stack (only from the top!)

The top of the stack represents the page which is currently showed to the user. The other pages in the stack are still available, and usually can be accessed by navigating back from the current page (for example by clicking the back button).

Even if routing may look simple on the surface, a modern routing system needs to consider many complex situations and usecases. That is why Flutter provides a built-in API to perform navigation.

How navigation is implemented in Flutter

The most basic setup for navigation in Flutter consists of:

  • An initial page to show when the app loads
  • A system to navigate from one page to another (and back)

Flutter allows to achieve this result in just a few steps.

First create a widget (class) for each page in the app:

first_route.dart
class FirstRoute extends StatelessWidget {
  const FirstRoute({super.key});
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Placeholder(),
    );
  }
}
second_route.dart
class SecondRoute extends StatelessWidget {
  const SecondRoute({super.key});
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Placeholder(),
    );
  }
}

Tip: Try to use a consistent naming convention for the all pages in your app. Usually all pages classes in the app should have a -Page, -Route, or -Screen suffix. This convention will help us later when implementing a more advanced configuration.

Pass your entry page as the home parameter inside MaterialApp:

main.dart
void main() {
  runApp(const MaterialApp(
    title: 'Flutter Navigation',
    home: FirstRoute(),
  ));
}

Finally use the Navigator API to push and pop pages in the stack.

/// Navigate to a new page using `push`
Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => const SecondRoute()),
);
 
/// Come back to the previous page using `pop`
Navigator.pop(context);

Note: If you call pop and the stack has only one page (initial), then the app will be closed.

Adding name routes in Flutter

The previous step is not ideal when the app starts to scale. A more powerful configuration allows you to define a name for each page in the app.

By doing this, you are not required to instantiate a new route class every time you push a new page. You can instead push a page by passing its name. Each name will have a unique association to a route class.

Named routes in Flutter are also super simple! Just define all the routes inside the routes parameter of MaterialApp:

main.dart
void main() {
  runApp(MaterialApp(
    home: FirstRoute(), // Initial route named '/'
    routes: <String, WidgetBuilder> {
      '/second': (BuildContext context) => SecondRoute(),
    },
  ));
}

You can still use the home parameter to define the initial page. The initial page will have '/' as default name.

Now instead of passing SecondRoute() to the push method, you can simply use its name by calling pushNamed:

/// Push route named `'/second'`
Navigator.pushNamed(context, '/second');

More advanced usecases

Flutter doesn't stop here! The API is a lot more powerful and allows you to do things like:

  • Passing arguments to pages
  • Return values to previous pages
  • Defining custom transitions between pages
  • And more!

You can learn more by reading the official Navigation cookbook, the Navigation 2.0 guide, or reading the Navigator class API.

There is more.

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.

The Flutter open-source ecosystem provides also many solutions to implement navigation. You can find many package built on top of Navigator that aims to simplify and expand the API. Some common solutions are:

In the rest of this post we are going to learn how to use the auto_route package to setup a type-safe and scalable routing system in Flutter.

The auto_route package allows to auto-generate the complete routing configuration for your app. All you need to do is define the pages in the app, and the package will take care of the rest.

Why do I suggest using auto_route? I have been using this package way before v1 was even published. The package improved a lot since then, and it has always been well maintained.

What I care most in my routing configuration is type-safety. I want the simplicity of the basic configuration (only defining the list of pages) with the power of having complete control over navigation and type-safety in the parameters passed to each page.

This is what auto_route unlocks for me. By using build_runner, the package inspects the list of pages and auto generates type-safe routes. It then exports a simple class that just works, without any extra configuration.

Installing auto_route

As mentioned, auto_route uses build_runner to auto generate routes. Because of this, we need to install 3 packages:

  • auto_route
  • auto_route_generator
  • build_runner
pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
 
  auto_route: ^4.2.1
 
dev_dependencies:
  flutter_test:
    sdk: flutter
 
  build_runner: ^2.2.0
  auto_route_generator: ^4.2.1

Defining the routes in the app

The second step is defining the list of pages in the app.

I usually create a folder called core inside lib, which I use for shared classes and configurations in the app. Inside core I then create a route folder.

In order to configure auto_route we need to create a file where we define the list of pages. Let's create a app_router.dart file inside the route folder. This file will contain a single empty class (AppRouter). This class is annotated using auto_route.

To generate a part-of file instead of a stand alone AppRouter class, simply add a Part Directive to your AppRouter and extend the generated private router.

app_router.dart
/// Make sure to import `auto_route` and `material` (required)
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_supabase_complete/app/home_page.dart';
 
part 'app_router.gr.dart';
 
/// You could also use one of the following:
/// - `@CupertinoAutoRouter`: Specific for IOS
/// - `@AdaptiveAutoRouter`: Adaptive based on platform              
/// - `@CustomAutoRouter`: Completely custom solution
@MaterialAutoRouter(
  replaceInRouteName: 'Page,Route',
  routes: <AutoRoute>[
    AutoRoute(page: HomePage, initial: true),
  ],
)
class AppRouter extends _$AppRouter {}

Make sure to import both auto_route and material. These classes are required inside the generated file, even if they are reported as unused in the original file.

In this basic configuration we defined two parameters:

  • replaceInRouteName: if you keep a consistent naming convention (adding the -Page suffix) auto_route can auto generate routes by replacing -Page with -Route, as defined in this parameter
  • routes: list of pages in the app. All you need to do is passing the name of the class (HomePage) and mark one page as initial

Complete the auto_route configuration

The third and final step is adding auto_route to MaterialApp.

First, run the build command to generate the routes using the package:

flutter packages pub run build_runner build

This command will generate a app_router.gr.dart file like the following:

app_router.gr.dart
// **************************************************************************
// AutoRouteGenerator
// **************************************************************************
 
// GENERATED CODE - DO NOT MODIFY BY HAND
 
// **************************************************************************
// AutoRouteGenerator
// **************************************************************************
//
// ignore_for_file: type=lint
 
part of 'app_router.dart';
 
class _$AppRouter extends RootStackRouter {
  _$AppRouter([GlobalKey<NavigatorState>? navigatorKey]) : super(navigatorKey);
 
  @override
  final Map<String, PageFactory> pagesMap = {
    HomeRoute.name: (routeData) {
      return MaterialPageX<dynamic>(
          routeData: routeData, child: const HomePage());
    }
  };
 
  @override
  List<RouteConfig> get routes => [RouteConfig(HomeRoute.name, path: '/')];
}
 
/// generated route for
/// [HomePage]
class HomeRoute extends PageRouteInfo<void> {
  const HomeRoute() : super(HomeRoute.name, path: '/');
 
  static const String name = 'HomeRoute';
}

We then only need to create an instance of AppRouter and pass it to MaterialApp.router:

main.dart
class App extends StatelessWidget {
  App({Key? key}) : super(key: key);
  final _appRouter = AppRouter();
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerDelegate: _appRouter.delegate(),
      routeInformationParser: _appRouter.defaultRouteParser(),
    );
  }
}

AppRouter is the class generated by auto_route, containing all the routing configurations ready to use.

Remove generated files from git

One last suggestion here is to remove the generated .gr.dart file from git. Add the following line to .gitignore:

.gitignore
# Generated files
lib/**/*.gr.dart

That is all! You learned how routing works in Flutter using the built-in API. Then we saw how to use the auto_route package to level up our routing configuration.

I used this configuration in many apps and it works well also at scale. If you found this post helpful, you can follow me at @SandroMaglione for more updates on Flutter and dart.

Thanks for reading.

👋・Interested in learning more, every week?

Every week I build a new open source project, with a new language or library, and teach you how I did it, what I learned, and how you can do the same. Join me and other 600+ readers.