Flutter ZERO Boilerplate Router with Auto Route – Flutter Navigation Tutorial

8  comments

Navigation and routing take just too much code and effort to implement. No matter if you implement a router yourself or you use a library like fluro or sailor, there's still a lot of boilerplate. All of this changes with the auto_route package which works elegantly by code generation. This way, you can reduce repetitive and error-prone code to a minimum while still having the ability to customize the routes to your heart's content.

What we will build

The starter project contains the individual page widgets already pre-built so that we can focus purely on navigation. At the end of this tutorial, we'll have an app with three routes, custom route arguments and custom transitions.

Adding dependencies

To follow along with this tutorial, use the same versions of the packages to make sure we're on the same page.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  auto_route: ^0.2.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner:
  auto_route_generator: ^0.2.1+3

The Router class

The center point of navigation with the auto_route library is a class annotated with @autoRouter. In its simplest yet fully functional form, the router class takes only a few lines of code.

routes/router.dart

@autoRouter
class $Router {
  InitialPage initialPage;
  SecondPage secondPage;
  ThirdPage thirdPage;
}
There is no part 'router.g.dart'; statement as you might be used to from other code gen packages. That's because the generated classes don't use the any members of our manually written class, hence, the generated file is not a "part" of the manually written file.

While even this simplest will work perfectly fine for basic routing, we can make the generated name of the InitialPage be a slash '/' as usual by annotating it with @initial.

routes/router.dart

@autoRouter
class $Router {
  @initial
  InitialPage initialPage;
  SecondPage secondPage;
  ThirdPage thirdPage;
}

After running every Flutter developer's favorite command, we'll have a generated router.gr.dart file.

👨‍💻 terminal

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

Plugging the Router into Flutter

The generated class contains three main things:

  • names of the routes as constant strings
  • onGenerateRoute method which handles navigation arguments out of the box (more on that below)
  • a navigatorKey & navigator properties used for navigating without passing around the BuildContext

Let's plug this all into the MaterialApp or CupertinoApp widget.

main.dart

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Material App',
      initialRoute: Router.initialPage,
      onGenerateRoute: Router.onGenerateRoute,
      navigatorKey: Router.navigatorKey,
    );
  }
}
Notice that we're not using the $Router class which we've defined ourselves. Always use the generated class without the dollar sign.

Single-argument routes ☝

The SecondPage widget takes in a single argument into its constructor

pages/second_page.dart

class SecondPage extends StatelessWidget {
  final String userId;

  const SecondPage({@required this.userId});
  ...
}

When auto_route sees a single-parameter constructor, it will make sure that the passed-in argument present and that it is of the correct type. Since auto_route uses the default navigator API with named routes, passing arguments to a new route uses the arguments parameter of the pushNamed method. Let's add the navigation code to the "Go to SECOND" button callback inside initial_page.dart!

pages/initial_page.dart

void navigateToSecond(BuildContext context) {
  Router.navigator.pushNamed(Router.secondPage, arguments: 'unique_user_id');
}

We can easily navigate without passing in the BuildContext by the virtue of the navigatorKey which we specified in main.dart. Additionally, if we pass in an argument of a wrong type or we leave it out completely, a prominent error page will be displayed.

Oh no! Argument mistype.
Should the constructor parameter not be marked @required, leaving out arguments completely wouldn't be an issue. Type checking would still occur though, which is good.

Multi-argument routes 🖐

The ThirdPage takes in two arguments:

pages/third_page.dart

class ThirdPage extends StatelessWidget {
  final String userName;
  final int points;

  const ThirdPage({
    @required this.userName,
    @required this.points,
  });
}

Will we be forced to use a Map<String, dynamic> by the auto_route package? 😱

Nope! 😅 The package will generate a ThirdPageArguments class honoring any @required annotations or default values there may be in the constructor. Now we can navigate to the third page from initial_page.dart.

pages/initial_page.dart

void navigateToThird(BuildContext context) {
  Router.navigator.pushNamed(
    Router.thirdPage,
    arguments: ThirdPageArguments(points: 123, userName: 'Bob'),
  );
}

ThirdPageArguments is the only type accepted by the generated Router. Passing in anything else will result in an error.

As soon as one page constructor parameter is marked @required, not passing in the generated 'PageName'Arguments object will result in an error.

Customizing routes

Our $Router class is currently pretty lean.

routes/router.dart

@autoRouter
class $Router {
  @initial
  InitialPage initialPage;
  SecondPage secondPage;
  ThirdPage thirdPage;
}

Navigating to any of the routes is equivalent to instantiating the simplest MaterialPageRoute without any additional configuration. Let's change that!

Passing in custom arguments, for example the fullscreenDialog boolean, is possible with @MaterialRoute or @CupertinoRoute annotations. The following will make the SecondPage appear with a cross instead of the back arrow in the AppBar.

routes/router.dart

@MaterialRoute(fullscreenDialog: true)
SecondPage secondPage;

Should you want to use neither material design nor cupertino for your routes, you can annotate the route with @CustomRoute which instantiates a PageRouteBuilder behind the scenes. Among other things, this allows you to specify custom transitions.

routes/router.dart

@CustomRoute(
  transitionsBuilder: TransitionsBuilders.zoomIn,
  durationInMilliseconds: 200,
)
ThirdPage thirdPage;

TransitionsBuilders comes from auto_route and it has a bunch of functions pre-defined. You can also create your own transition animations by using the default functions as a template.

Learn more about custom transitions and other features of auto_route which you may not use on a daily basis, but that are still useful from the well-written official docs.

It's truly invigorating to see the whole Flutter package ecosystem become more mature. The auto_route package surely contributes a ton to developer productivity and happiness since handling routing manually is a toilsome endeavor. Be sure to show the author, Milad Akarie, some support by giving this package a like on pub.dev and a star on GitHub!

About the author 

Matt Rešetár

Matt is an app developer with a knack for teaching others. Working as a Flutter freelancer and most importantly developer educator, he doesn't have a lot of free time 😅 Yet he still manages to squeeze in tough workouts 💪 and guitar 🎸

You may also like

Flutter Custom & Staggered Page Transition Animation Tutorial

Flutter Firebase & DDD Course [5] – Sign-In Form Logic

  • It seems like CustomRoute’s usefulness is limited by its transitions builder’s signature – so this doesn’t allow for animations to the “previous route” when navigating to a new route.

    Unless I am missing something, it seems like this package doesn’t support transitions like “make OldPage exit left while NewPage enters from the right”.

  • thanks for yet another great Flutter tutorial ! are you able to add a segment showing how it could be used to maintain navigation state when using a bottom nav bar ?

  • Great tutorial, Matt! I’m playing with RouteGuard but I’m in a bit of a dilemma: in the documentation it says that the main() method is a good place to register RouteGuards but my RouteGuard class depends on an object that Provider instantiates inside MyApp widget, so when my RouteGuard object is instantiated Provider.of cannot find the dependency yet and an error is thrown. I tried to instantiate my Provider before runApp(MyApp()) but I get all sort of errors. Am I making sense?

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