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:
class FirstRoute extends StatelessWidget {
const FirstRoute({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Placeholder(),
);
}
}
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
:
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
:
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 🤩
Timeless coding principles, practices, and tools that make a difference, regardless of your language or framework, delivered in your inbox every week.
Navigation in Flutter using a package
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.
Navigation in Flutter using the auto_route
package
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
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 yourAppRouter
and extend the generated private router.
/// 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
andmaterial
. 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 parameterroutes
: list of pages in the app. All you need to do is passing the name of the class (HomePage
) and mark one page asinitial
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:
// **************************************************************************
// 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
:
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 byauto_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
:
# 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.