Learning Flutter’s new navigation and routing system

John Ryan
John Ryan
Sep 30, 2020 · 9 min read

This article explains how Flutter’s new Navigator and Router API works. If you follow Flutter’s open design docs, you might have seen these new features referred to as Navigator 2.0 and Router. We’ll explore how these APIs enable more fine-tuned control over the screens in your app and how you can use it to parse routes.

These new APIs are not breaking changes, they simply add a new declarative API. Before Navigator 2.0, it was difficult to push or pop multiple pages, or remove a page underneath the current one. However, if you are happy with how the Navigator works today, you can keep using it in the same (imperative) way.

The Router provides the ability to handle routes from the underlying platform and display the appropriate pages. In this article, the Router is configured to parse the browser URL to display the appropriate page.

This article helps you choose which Navigator pattern works best for your app, and explains how to use Navigator 2.0 to parse browser URLs and take full control over the stack of pages that are active. The exercise in this article shows how to build an app that handles incoming routes from the platform and manages the pages of your app. The following GIF shows the example app in action:

Navigator 1.0

  • Navigator — a widget that manages a stack of Route objects.
  • Route — an object managed by a Navigator that represents a screen, typically implemented by classes like MaterialPageRoute.

Before Navigator 2.0, Routes were pushed and popped onto the Navigator’s stack with either named routes or anonymous routes. The next sections are a brief recap of these two approaches.

Anonymous routes

MaterialApp and CupertinoApp already use a Navigator under the hood. You can access the navigator using Navigator.of() or display a new screen using Navigator.push(), and return to the previous screen with Navigator.pop():

When push() is called, the DetailScreen widget is placed on top of the HomeScreen widget like this:

The previous screen (HomeScreen) is still part of the widget tree, so any State object associated with it stays around while DetailScreen is visible.

Named routes

These routes must be predefined. Although you can pass arguments to a named route, you can’t parse arguments from the route itself. For example, if the app is run on the web, you can’t parse the ID from a route like /details/:id.

Advanced named routes with onGenerateRoute

Here’s the complete example:

Here, settings is an instance of RouteSettings. The name and arguments fields are the values that were provided when Navigator.pushNamed was called, or what initialRoute is set to.

Navigator 2.0

  • Page — an immutable object used to set the navigator’s history stack.
  • Router — configures the list of pages to be displayed by the Navigator. Usually this list of pages changes based on the underlying platform, or on the state of the app changing.
  • RouteInformationParser, which takes the RouteInformation from RouteInformationProvider and parses it into a user-defined data type.
  • RouterDelegate — defines app-specific behavior of how the Router learns about changes in app state and how it responds to them. Its job is to listen to the RouteInformationParser and the app state and build the Navigator with the current list of Pages.
  • BackButtonDispatcher — reports back button presses to the Router.

The following diagram shows how the RouterDelegate interacts with the Router, RouteInformationParser, and the app’s state:

Here’s an example of how these pieces interact:

  1. When the platform emits a new route (for example, “books/2”) , the RouteInformationParser converts it into an abstract data type T that you define in your app (for example, a class called BooksRoutePath).
  2. RouterDelegate’s setNewRoutePath method is called with this data type, and must update the application state to reflect the change (for example, by setting the selectedBookId) and call notifyListeners.
  3. When notifyListeners is called, it tells the Router to rebuild the RouterDelegate (using its build() method)
  4. RouterDelegate.build() returns a new Navigator, whose pages now reflect the change to the app state (for example, the selectedBookId).

Navigator 2.0 exercise

To follow along, switch to the master channel, create a new Flutter project with web support, and replace the contents of lib/main.dart with the following:


In _BooksAppState, keep two pieces of state: a list of books and the selected book:

Then in _BooksAppState, return a Navigator with a list of Page objects:

Since this app has two screens, a list of books and a screen showing the details, add a second (detail) page if a book is selected (using collection if):

Note that the key for the page is defined by the value of the book object. This tells the Navigator that this MaterialPage object is different from another when the Book object is different. Without a unique key, the framework can’t determine when to show a transition animation between different Pages.

Note: If you prefer, you can also extend Page to customize the behavior. For example, this page adds a custom transition animation:

Finally, it’s an error to provide a pages argument without also providing an onPopPage callback. This function is called whenever Navigator.pop() is called. It should be used to update the state (that determines the list of pages), and it must call didPop on the route to determine if the pop succeeded:

It’s important to check whether didPop fails before updating the app state.

Using setState notifies the framework to call the build() method, which returns a list with a single page when _selectedBook is null.

Here’s the full example:

As it stands, this app only enables us to define the stack of pages in a declarative way. We aren’t able to handle the platform’s back button, and the browser’s URL doesn’t change as we navigate.


This section shows how to implement the RouteInformationParser, RouterDelegate, and update the app state. Once set up, the app stays in sync with the browser’s URL.

Data types

In this app, all of the routes in the app can be represented using a single class. Instead, you might choose to use different classes that implement a superclass, or manage the route information in another way.


The generic type defined on RouterDelegate is BookRoutePath, which contains all the state needed to decide which pages to show.

We’ll need to move some logic from _BooksAppState to BookRouterDelegate, and create a GlobalKey. In this example, the app state is stored directly on the RouterDelegate, but could also be separated into another class.

In order to show the correct path in the URL, we need to return a BookRoutePath based on the current state of the app:

Next, the build method in a RouterDelegate needs to return a Navigator:

The onPopPage callback now uses notifyListeners instead of setState, since this class is now a ChangeNotifier, not a widget. When the RouterDelegate notifies its listeners, the Router widget is likewise notified that the RouterDelegate's currentConfiguration has changed and that its build method needs to be called again to build a new Navigator.

The _handleBookTapped method also needs to use notifyListeners instead of setState:

When a new route has been pushed to the application, Router calls setNewRoutePath, which gives our app the opportunity to update the app state based on the changes to the route:


This implementation is specific to this app, not a general route parsing solution. More on that later.

To use these new classes, we use the new MaterialApp.router constructor and pass in our custom implementations:

Here’s the complete example:

Running this sample in Chrome now shows the routes as they are being navigated, and navigates to the correct page when the URL is manually edited.


Provide a custom TransitionDelegate to a Navigator that defines the desired behavior:

For example, the following implementation disables all transition animations:

This custom implementation overrides resolve(), which is in charge of marking the various routes as either pushed, popped, added, completed, or removed:

  • markForPush — displays the route with an animated transition
  • markForAdd — displays the route without an animated transition
  • markForPop — removes the route with an animated transition and completes it with a result. “Completing” in this context means that the result object is passed to the onPopPage callback on AppRouterDelegate.
  • markForComplete — removes the route without a transition and completes it with a result
  • markForRemove — removes the route with no animated transition and without completing.

This class only affects the declarative API, which is why the back button still displays a transition animation.

How this example works: This example looks at both the new routes and the routes that are exiting the screen. It goes through all of the objects in newPageRouteHistory and marks them to be added without a transition animation using markForAdd. Next, it loops through values of the locationToExitingPageRoute map. If it finds a route marked as isWaitingForExitingDecision, then it calls markForRemove to indicate that the route should be removed without a transition and without completing.

Here’s the full sample(Gist).

Nested routers

Nested router sample(Gist)

What’s next


Flutter is Google's mobile UI framework for crafting…


Flutter is Google's mobile UI framework for crafting high-quality native interfaces on iOS, Android, web, and desktop. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source. Learn more at https://flutter.dev

John Ryan

Written by

John Ryan


Flutter is Google's mobile UI framework for crafting high-quality native interfaces on iOS, Android, web, and desktop. Flutter works with existing code, is used by developers and organizations around the world, and is free and open source. Learn more at https://flutter.dev