Home Flutter Tutorials

Flutter Navigator 2.0 and Deep Links

With Flutter’s Navigator 2.0, learn how to handle deep links in Flutter and gain the ultimate navigation control for your app.

3.4/5 5 Ratings


  • Dart 2.10, Flutter 1.22, Android Studio 4.1

Flutter’s first stable release was in December 2018, and it has grown rapidly since then. As is the case with any software, the developers who built it or use it are constantly refining it with each new version.

Initially, navigation between pages in Flutter was possible with Navigator 1.0 only, which could push and pop pages. Most use cases required basic navigation that wasn’t a problem for Navigator 1.0. But with the introduction of more complex navigation use cases — especially after Flutter for Web came out — developers wanted the ability to add multiple pages in one go or remove any offscreen page(s) on the onscreen page. To cover these use cases, the Flutter team introduced Navigator 2.0.

Note: In Flutter, screens and pages are called routes. However, in this tutorial you will mostly see screens or pages, and routes a few times. They mean the same thing for the most part.

In this tutorial, you’ll learn how to use Navigator 2.0 by building pages of a shopping app brilliantly called, Navigation App :]. You’ll also learn how it can provide much more granular control for your app’s navigation and deep linking. To do so, you’ll learn how to implement:

  • RouterDelegate
  • RouteInformationParser
  • BackButtonDispatcher

This tutorial uses Android Studio, but Visual Studio Code or IntelliJ IDEA will work fine as well.

Note: Navigator 2.0 is backward-compatible, and if needed, you can gradually introduce it to your existing apps that use Navigator 1.0 — without introducing any breaking changes.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the page.

The starter app is a set of screens for the shopping app. The UI doesn’t do much, but it shows how to navigate between pages. This set of pages — represented as screens — is in the image below:

The app starts with the Splash page, which shows an image:

Splash Page with image

Run your app and verify it opens to this page. The app will stay on this page since the navigation system isn’t yet implemented. Gradually, you’ll add code to navigate between all screens.

Once you implement the navigation, the app should display the Splash page for a short duration and then show the Login page if the user isn’t logged in. If they are, they’ll see the Shopping List page instead. This logged-in state is saved as a Boolean value in the app’s local storage using the shared_preferences package.

The user can go to the Create Account page for signing up or stay on the Login page, where they can log in and then navigate to the Shopping List page.

Note that this tutorial won’t cover the implementation of a functional login system or any real-world shopping app features. The screens and their corresponding code mimic their UI to explain the navigation concept.

Navigator 1.0

Navigator 1.0 uses the Navigator and Route classes to navigate between pages. If you want to add, or push, a page, use:

  MaterialPageRoute(builder: (context) {
    return MyNewScreen();

Here, MaterialPageRoute returns an instance of your new screen, i.e. MyNewScreen.

To remove, or pop, the current page, use:


These operations are straightforward, but things get interesting if you have a use case that requires recreating a set of pages, such as for deep linking.

In the context of mobile apps, deep linking consists of using a uniform resource identifier (URI) that links to a specific location within a mobile app rather than launching the app.

For example, while building an e-commerce app, you may want the user to go to a product page when they tap the “Product X is now on Sale!” notification. To handle this, the app needs to clear the current navigation stack of the app, add the home screen that displays products in a list and then add the product page to this stack of pages. With Navigator 1.0, this is difficult.

Luckily, Navigator 2.0 provides a lot more flexibility for such a use case.

Navigator 2.0

Unlike Navigator 1.0, which uses an imperative style of programming, Navigator 2.0 uses a declarative style. As a result, it feels more “Flutter-like”. Understanding Navigator 2.0 involves understanding a few of its concepts such as:

  • Page: An abstract class that describes the configuration of a route
  • Router: A class that manages opening and closing pages of an application
  • RouteInformationParser: An abstract class used by the Router‘s widget to parse route information into a configuration
  • RouteInformationProvider: An abstract class that provides route information for the Router‘s widget
  • RouterDelegate: An abstract class used by the Router‘s widget to build and configure a navigating widget
  • BackButtonDispatcher: Reports to a Router when the user taps the back button on platforms that support back buttons (such as Android)
  • TransitionDelegate: The delegate that decides how pages transition in or out of the screen when it’s added or removed.

This article doesn’t cover TransitionDelegate, as in most use cases, DefaultTransitionDelegate does a good job with transitions. If you need to handle transitions between pages in a unique way, you can create your own delegate by extending TransitionDelegate.

Here’s a visual representation of the concepts mentioned above:

The classes in gray are optional to implement when using Navigator 2.0. However, the ones in blue, i.e. RouteInformationParser and RouterDelegate, must be implemented to use Navigator 2.0. You’ll learn about both of these, and the optional BackButtonDispatcher, in the sections below.

Pages Overview

Before you begin to implement any navigation, the next sections will provide an overview of the pages in your starter app.

Login Page

The Login page is the first page to appear after the Splash page if the user hasn’t already logged in:

Login page

Clicking on the Create Account button takes the user to the Create Account page, and pressing the Login button takes the user to the Shopping List page. When pressing the Login button, set the logged-in flag using Shared Preferences to true so that the next time the user logs in, they go straight to the Shopping List page after the Splash page.

Create Account Page

The Create Account page looks almost the same as the Login page:

Create Account page

The only difference here is the user can use the Cancel button, tap the back arrow icon or press the device back button to go back to the Login page.

Shopping List Page

This page displays a list of items which is a hardcoded list of dummy data to simulate a shopping list:

Shopping List page

When the user taps an item in this list, it takes them to the Details page. The item number clicked is passed to the Details page as a constructor argument.

This page also supports two actions in the AppBar, which denote Settings and Cart. Tapping these will take the user to the respective pages.

Details Page

The Details page shows item details:

Item Details page

This screen shows the buttons Add to Cart and Cart. The Add to Cart button will add the item to an internal list that mimics a cart, and will take the user back to the Shopping List page. The Cart button will take the user to the Cart page.

Cart Page

This page shows the items in the cart:

Cart page

It has an AppBar action for navigating to the Checkout page.

Checkout Page

The Checkout page shows the items in the cart:

Checkout page

It has two buttons: one to go back to the Shopping List page, and another to clear the cart.

Settings Page

This page allows the user to log out:

Settings page

Afterward, they’ll return to the Login page. This will also reset the logged-in flag to false to preserve this state.

Pages Setup

Now that you’re aware of all the pages the app displays, you’ll need some information about the pages to represent them in the app. This information shall be captured in a class named PageConfiguration.

In the lib directory of the starter project, create a new directory called router. In the router directory, create a new Dart file named ui_pages.dart and add the following to it:

const String SplashPath = '/splash';
const String LoginPath = '/login';
const String CreateAccountPath = '/createAccount';
const String ListItemsPath = '/listItems';
const String DetailsPath = '/details';
const String CartPath = '/cart';
const String CheckoutPath = '/checkout';
const String SettingsPath = '/settings';

The constants above define the paths or routes of each screen. Next, it’s important to represent the UI for each page. Begin by adding an enum for this within the same file:

enum Pages {

Finally, in the same file, create the PageConfiguration class mentioned earlier that combines all the information about each page you defined above:

class PageConfiguration {
  final String key;
  final String path;
  final Pages uiPage;

  const PageConfiguration(
      {@required this.key, @required this.path, @required this.uiPage});

PageConfiguration holds two Strings, which represent the page’s key and path. And then a third parameter which represents the UI associated with that page using the Pages enum you added earlier.

Use PageConfiguration to define data or information about each of the pages of the app, as shown below. Note that you’re still adding code to ui_pages.dart:

const PageConfiguration SplashPageConfig =
    PageConfiguration(key: 'Splash', path: SplashPath, uiPage: Pages.Splash);
const PageConfiguration LoginPageConfig =
    PageConfiguration(key: 'Login', path: LoginPath, uiPage: Pages.Login);
const PageConfiguration CreateAccountPageConfig = PageConfiguration(
    key: 'CreateAccount', path: CreateAccountPath, uiPage: Pages.CreateAccount);
const PageConfiguration ListItemsPageConfig = PageConfiguration(
    key: 'ListItems', path: ListItemsPath, uiPage: Pages.List);
const PageConfiguration DetailsPageConfig =
    PageConfiguration(key: 'Details', path: DetailsPath, uiPage: Pages.Details);
const PageConfiguration CartPageConfig =
    PageConfiguration(key: 'Cart', path: CartPath, uiPage: Pages.Cart);
const PageConfiguration CheckoutPageConfig = PageConfiguration(
    key: 'Checkout', path: CheckoutPath, uiPage: Pages.Checkout);
const PageConfiguration SettingsPageConfig = PageConfiguration(
    key: 'Settings', path: SettingsPath, uiPage: Pages.Settings);

For the Splash page, declare a constant named SplashPageConfig that represents the page’s PageConfiguration. This means it’s of the type PageConfiguration. The first parameter of PageConfiguration represents the key 'Splash'. The second argument represents the Splash page’s path, SplashPath. Finally, the third argument represents the UI associated with the Splash page, i.e. Pages.Splash.

The same analogy defines PageConfiguration for all other pages below this one.


RouterDelegate contains the core logic for Navigator 2.0. This includes the control for navigating between pages. This class is an abstract class that requires classes that extend RouterDelegate to implement all of its unimplemented methods.

Name the RouterDelegate for this app ShoppingRouterDelegate. Begin by creating a new Dart file in the router directory called router_delegate.dart. Add the following import statements:

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../ui/details.dart';
import '../ui/cart.dart';
import '../ui/checkout.dart';
import '../ui/create_account.dart';
import '../ui/list_items.dart';
import '../ui/login.dart';
import '../ui/settings.dart';
import '../ui/splash.dart';
import 'ui_pages.dart';

This includes imports for all the pages you created in the previous section and their UI widgets. Next, add the code representing the basic structure of this app’s RouterDelegate, i.e. ShoppingRouterDelegate:

// 1
class ShoppingRouterDelegate extends RouterDelegate<PageConfiguration>
    // 2
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<PageConfiguration> {
  // 3
  final List<Page> _pages = [];

  // 4
  PageConfiguration get currentConfiguration => _pages.last.arguments as PageConfiguration;

  // 5
  GlobalKey<NavigatorState> get navigatorKey => GlobalKey<NavigatorState>();


Ignore the errors for now, you will resolve them soon. Here’s what’s happening in the code above:

  1. This represents the app’s RouterDelegate, ShoppingRouterDelegate. It extends the abstract RouterDelegate, which produces a configuration for each Route. This configuration is PageConfiguration.
  2. ShoppingRouterDelegate uses the ChangeNotifier mixin, which helps notify any listeners of this delegate to update themselves whenever notifyListeners() is invoked. This class also uses PopNavigatorRouterDelegateMixin, which lets you remove pages. It’ll also be useful later when you implement BackButtonDispatcher.
  3. This list of Pages is the core of the app’s navigation, and it denotes the current list of pages in the navigation stack. It’s private so that it can’t be modified directly, as that could lead to errors and unwanted states. You’ll see later how to handle modifying the navigation stack without writing to this list directly from anywhere outside ShoppingRouterDelegate.
  4. currentConfiguration gets called by Router when it detects route information may have changed. “current” means the topmost page of the app i.e. _pages.last. This getter returns configuration of type PageConfiguration as defined on line 1 while creating RouterDelegate<PageConfiguration>. The currentConfiguration for this last page can be accessed as _pages.last.arguments.
  5. PopNavigatorRouterDelegateMixin requires a navigatorKey used for retrieving the current navigator of the Router.

Next, you’ll implement build.

Implementing build

One of the methods from RouterDelegate that you’ll implement is build. It gets called by RouterDelegate to obtain the widget tree that represents the current state. In this scenario, the current state is the navigation history of the app. As such, use Navigator to implement build by adding the code below:

Widget build(BuildContext context) {
  return Navigator(
    key: navigatorKey,
    onPopPage: _onPopPage,
    pages: List.of(_pages),

Navigator uses the previously defined navigatorKey as its key. pages uses a modifiable list of the private _pages list, which represents the app’s navigation stack. Finally, Navigator needs to know what to do when the app requests the removal or popping of a page via a back button press using onPopPage.

Removing Pages

To remove pages, define a private _onPopPage method:

bool _onPopPage(Route<dynamic> route, result) {
  // 1
  final didPop = route.didPop(result);
  if (!didPop) {
    return false;
  // 2
  // 3

  return true;

This method will be called when pop is invoked, but the current Route corresponds to a Page found in the pages list.

The result argument is the value with which the route completed. An example of this is the value returned from a dialog when it’s popped.

In the code above:

  1. There’s a request to pop the route. If the route can’t handle it internally, it returns false.
  2. Otherwise, remove RouteSettings from the list of pages. It might seem weird that RouteSettings is being removed from a List of pages here, but if you look at the definition of Page, you’ll see that it extends RouteSettings.
  3. Notify listeners via ChangeNotifier. This lets you do things such as triggering analytic events when a user-facing screen changes.

It’s possible you’ll want to remove a page from the navigation stack. To do this, create a private method _removePage. This modifies the internal _pages field:

void _removePage(MaterialPage page) {
  if (page != null) {

_removePage is a private method, so to access it from anywhere in the app, use RouterDelegate‘s popRoute method. Add this as an overriden method:

Future<bool> popRoute() {
  if (_pages.length > 1) {
    return Future.value(true);
  return Future.value(false);

This method ensures there are at least two pages in the list and then calls _removePage to remove it and return true. Otherwise, don’t call _removePage, but return false to close the app. If you don’t add _pages.length > 1, check here and call popRoute on the last page of the app, where you’ll see a blank screen.

Creating and Adding a Page

Now that you know how to remove a page, you’ll write code to create and add a page. You’ll use MaterialPage, which is a Page subclass provided by the Flutter SDK:

MaterialPage _createPage(Widget child, PageConfiguration pageConfig) {
  return MaterialPage(
      child: child,
      key: Key(pageConfig.key),
      name: pageConfig.path,
      arguments: pageConfig

The arguments for this method include a Widget. This widget will be the UI displayed to the user when they’re on this page. The second argument is an object of type PageConfiguration, which holds the configuration of the page this method creates.

The first three parameters of MaterialPage are straightforward. The fourth parameter is arguments, and the pageConfig is passed to it. This lets you easily access the configuration of the page if needed.

Now that there’s a method to create a page, create another method to add this page to the navigation stack, i.e. to the _pages list:

void _addPageData(Widget child, PageConfiguration pageConfig) {
    _createPage(child, pageConfig),

The public method for adding a page is addPage. You’ll implement it using the Pages enum:

void addPage(PageConfiguration pageConfig) {
  // 1
  final shouldAddPage = _pages.isEmpty ||
        (_pages.last.arguments as PageConfiguration).uiPage !=

  if (shouldAddPage) {
    // 2
    switch (pageConfig.uiPage) {
      case Pages.Splash:
        // 3
        _addPageData(Splash(), SplashPageConfig);
      case Pages.Login:
        _addPageData(Login(), LoginPageConfig);
      case Pages.CreateAccount:
        _addPageData(CreateAccount(), CreateAccountPageConfig);
      case Pages.List:
        _addPageData(ListItems(), ListItemsPageConfig);
      case Pages.Cart:
        _addPageData(Cart(), CartPageConfig);
      case Pages.Checkout:
        _addPageData(Checkout(), CheckoutPageConfig);
      case Pages.Settings:
        _addPageData(Settings(), SettingsPageConfig);

    // 4

The code above does the following:

  1. Decides whether to add a new page. The second condition ensures the same page isn’t added twice by mistake. Example: You wouldn’t want to add a Login page immediately on top of another Login page.
  2. Uses a switch case on the pageConfig‘s UI_PAGE so you know which page to add.
  3. Uses the recently created private addPageData to add the widget and PageConfiguration associated with the corresponding UI_PAGE from the switch case.
  4. Notifies listeners via ChangeNotifier.

You’ll notice switch doesn’t handle the Details page case. That’s because adding that page requires another argument, which you’ll read about later.

Modifying the Contents

Now comes the fun part. Create some methods that allow you to modify the contents of the _pages list. To cover all use cases of the app, you’ll need methods to add, delete and replace the _pages list:

// 1
void replace(PageConfiguration newRoute) {
  if (_pages.isNotEmpty) {

// 2
void setPath(List<MaterialPage> path) {

// 3
void replaceAll(PageConfiguration newRoute) {

// 4
void push(PageConfiguration newRoute) {

// 5
void pushWidget(Widget child, PageConfiguration newRoute) {
  _addPageData(child, newRoute);

Here’s a breakdown of the code above:

  1. replace method: Removes the last page, i.e the top-most page of the app, and replaces it with the new page using the add method
  2. setPath method: Clears the entire navigation stack, i.e. the _pages list, and adds all the new pages provided as the argument
  3. replaceAll method: Calls setNewRoutePath. You’ll see what this method does in a moment.
  4. push method: This is like the addPage method, but with a different name to be in sync with Flutter’s push and pop naming.
  5. pushWidget method: Allows adding a new widget using the argument of type Widget. This is what you’ll use for navigating to the Details page.

The last piece of the RouterDelegate puzzle is to override setNewRoutePath, which is also the method called by replaceAll above. This method clears the list and adds a new page, thereby replacing all the pages that were there before:

Future<void> setNewRoutePath(PageConfiguration pageData) {
  return Future.value(null);

RouterDelegate is a lot to take in. Take a break to digest what you just learned. :]


A RouteInformationParser is a delegate used by Router to parse a route’s information into a configuration of any type T which in your case would be PageConfiguration.

This app’s RouteInformationParser is also known as ShoppingParser. Make this class extend from RouteInformationParser. To begin, create a new Dart file, shopping_parser.dart, in the router directory and add the following code to this file:

import 'package:flutter/material.dart';
import 'ui_pages.dart';

class ShoppingParser extends RouteInformationParser<PageConfiguration> {

RouterInformationParser requires that its subclasses override parseRouteInformation and restoreRouteInformation.

parseRouteInformation converts the given route information into parsed data — PageConfiguration in this case — to pass to RouterDelegate:

Future<PageConfiguration> parseRouteInformation(
      RouteInformation routeInformation) async {
  // 1
  final uri = Uri.parse(routeInformation.location);
  // 2
  if (uri.pathSegments.isEmpty) {
    return SplashPageConfig;

  // 3
  final path = uri.pathSegments[0];
    // 4
  switch (path) {
    case SplashPath:
      return SplashPageConfig;
    case LoginPath:
      return LoginPageConfig;
    case CreateAccountPath:
      return CreateAccountPageConfig;
    case ListItemsPath:
      return ListItemsPageConfig;
    case DetailsPath:
      return DetailsPageConfig;
    case CartPath:
      return CartPageConfig;
    case CheckoutPath:
      return CheckoutPageConfig;
    case SettingsPath:
      return SettingsPageConfig;
      return SplashPageConfig;

Here’s what’s happening in the code above:

  1. location from routeInformation is a String that represents the location of the application. The string is usually in the format of multiple string identifiers with slashes between — for example: `/`, `/path` or `/path/to/the/app`. It’s equivalent to the URL in a web application. Use parse from Uri to create a Uri from this String.
  2. If there are no paths, which is most likely the case when the user is launching the app, return SplashPage.
  3. Otherwise, get the first path segment from the pathSegements list of the uri.
  4. Then return the PageConfiguration corresponding to this first path segment.

restoreRouteInformation isn't required if you don't opt for the route information reporting, which is mainly used for updating browser history for web applications. If you decide to opt in, you must also override this method to return RouteInformation based on the provided PageConfiguration.

So, override restoreRouteInformation. In a way, this method does the exact opposite of the previously defined parseRouteInformation by taking in a PageDate and returning an object of type RouteInformation:

RouteInformation restoreRouteInformation(PageConfiguration configuration) {
  switch (configuration.uiPage) {
    case Pages.Splash:
      return const RouteInformation(location: SplashPath);
    case Pages.Login:
      return const RouteInformation(location: LoginPath);
    case Pages.CreateAccount:
      return const RouteInformation(location: CreateAccountPath);
    case Pages.List:
      return const RouteInformation(location: ListItemsPath);
    case Pages.Details:
      return const RouteInformation(location: DetailsPath);
    case Pages.Cart:
      return const RouteInformation(location: CartPath);
    case Pages.Checkout:
      return const RouteInformation(location: CheckoutPath);
    case Pages.Settings:
      return const RouteInformation(location: SettingsPath);
      return const RouteInformation(location: SplashPath);

This method uses uiPage from Page to return a RouteInformation with its location set to the given path. Notice that there's a RouteInformation with the location of SplashPath in case there are no matches for uiPage.

Root Widget and Router

Now that you have all the required Router classes, hook them up with the root widget of your app in the main.dart file. First, change the imports as follows:

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'cart_holder.dart';
import 'router/ui_pages.dart';
import 'router/router_delegate.dart';
import 'router/shopping_parser.dart';

This includes an import from the Get package. This is useful for state management, dependency injection and route management, but you'll use it for dependency injection. Don't forget to add its dependency in the pubspec.yaml file.

Next, create instances of ShoppingRouterDelegate and ShoppingParser. Do this at the line marked: // TODO Create Delegate, Parser and Back button Dispatcher:

final delegate = ShoppingRouterDelegate();
final parser = ShoppingParser();

Then, replace //TODO Setup Router with the following:

// 1
// 2

In the code above, you:

  • Set up the initial route of this app to be the Splash page using setNewRoutePath.
  • Use Get to inject an instance of the delegate in memory for later access from anywhere within the app.

For most Flutter apps, you might have MaterialApp or CupertinoApp as the root widget. Both of these use WidgetsApp internally. WidgetsApp creates a Router or a Navigator internally. In case of a Router, the Navigator is configured via the provided routerDelegate. Navigator then manages the pages list that updates the app's navigation whenever this list of pages changes.

Since Navigator 2.0 is backward-compatible with Navigator 1.0, the easiest way to start with Navigator 2.0 is to use MaterialApp's MaterialApp.router(...) constructor. This requires you to provide instances of a RouterDelegate and a RouteInformationParser as the ones discussed above.

Hence, in the root widget's build method, replace MaterialApp with:

return MaterialApp.router(
  title: 'Navigation App',
  debugShowCheckedModeBanner: false,
  theme: ThemeData(
      primarySwatch: Colors.blue,
      visualDensity: VisualDensity.adaptivePlatformDensity,
  routerDelegate: delegate,
  routeInformationParser: parser,

Notice that you're passing in the created routerDelegate and routeInformationParser. Run the app to make sure it still works. You'll still only see the Splash screen for now.

Navigating Between Pages

Now you'll cover how to navigate between the various pages.

Splash Page Navigation

Open splash.dart from the ui folder and add the following imports:

import 'package:get/get.dart';
import '../router/router_delegate.dart';
import '../router/ui_pages.dart';

Now, find // TODO launch list items or login and replace it with:

// 1
final delegate = Get.find<ShoppingRouterDelegate>();
// 2
if (loggedIn) {
// 3
} else {
// 4

Here's what's happening in the code above:

  1. Find the ShoppingRouterDelegate's instance, i.e. delegate from Get.
  2. Check if the user is already logged in.
  3. If yes, replace the Splash page with the List Items page.
  4. Otherwise, replace the Splash page with the Login page.

Hot reload your app and you'll see the app navigates to the Login page from the Splash page after two seconds. This duration is defined using a Timer on the Splash page in the splash.dart file.

Login Page Navigation

Now, open login.dart and add the following imports:

import 'package:get/get.dart';
import '../router/router_delegate.dart';
import '../router/ui_pages.dart';

Now, find the first TODO // TODO get delegate and replace it with:

final delegate = Get.find<ShoppingRouterDelegate>();

This gets the ShoppingRouterDelegate's delegate instance.

Find // TODO Go to Create Account and replace the onPressed method below it with:

onPressed: () => delegate.push(CreateAccountPageConfig),

This will take the user to the Create Account page. Find // TODO Go to List Items page and add:


List Items Page

Open the list_items.dart file and make similar changes.

Add imports:

import 'package:get/get.dart';
import '../router/router_delegate.dart';
import '../router/ui_pages.dart';
import 'details.dart';

Next, find // TODO get delegate and replace it with:

final delegate = Get.find<ShoppingRouterDelegate>();

This gets the ShoppingRouterDelegate's delegate instance.

Find // TODO Push Settings Page and replace the first IconButton widget and this TODO with:

IconButton(icon: Icon(Icons.settings), onPressed: () => delegate.push(SettingsPageConfig)),
IconButton(icon: Icon(Icons.add_shopping_cart_sharp), onPressed: () => delegate.push(CheckoutPageConfig))

This will navigate the user to the Settings page and Checkout page, respectively.

Find // TODO Push Details Widget and replace it with:

delegate.pushWidget(Details(index), DetailsPageConfig);

This will push the Details page on top of the List Items page.

Note: Here, you'll use pushWidget from ShoppingRouterDelegate instead of push, because the List Items page has to pass the index of the item in the list that the user tapped on and navigate to that specific item's Detail page.

This will take the user to the List Items page. Hot reload and try to verify each of these navigations by clicking on the associated buttons.

For the Create Account, List Items, Details, Cart, Checkout and Settings pages, follow the same format. Add the import statements, initialize the ShoppingRouterDelegate, find the TODO statements to know the various routing methods to call and what arguments to pass. If you are unsure at any point in time, refer to the final project.

Hot reload your app and verify the navigation discussed above to make sure everything works as expected. If you need to restart the entire navigation flow, go to the Settings page using the settings icon in the AppBar on the List Items page and press the Log Out button.


Navigator 2.0 also uses a BackButtonDispatcher class to handle system back button presses. If you want to create a custom dispatcher, you can create a subclass of RootBackButtonDispatcher.

This app’s BackButtonDispatcher is also known as ShoppingBackButtonDispatcher. Create this class by creating a new Dart file named back_dispatcher.dart in the router directory and adding the following code:

import 'package:flutter/material.dart';
import 'router_delegate.dart';

// 1
class ShoppingBackButtonDispatcher extends RootBackButtonDispatcher {
  // 2
  final ShoppingRouterDelegate _routerDelegate;

      : super();

  // 3
  Future<bool> didPopRoute() {
    return _routerDelegate.popRoute();

In the code above:

  1. Make ShoppingBackButtonDispatcher extend RootBackButtonDispatcher.
  2. Declare a final instance of ShoppingRouterDelegate. This helps you link the dispatcher to the app’s RouterDelegate, i.e. ShoppingRouterDelegate.
  3. Delegate didPopRoute to _routerDelegate.

Note that this class doesn’t do any complex back button handling here. Rather, it’s just an example of subclassing RootBackButtonDispatcher to create a custom Back Button Dispatcher. If you need to do some custom back button handling, add your code to didPopRoute().

To use this class open main.dart, add the import statement and add the initializing code after final parser = ShoppingParser();:

import 'router/back_dispatcher.dart';


ShoppingBackButtonDispatcher backButtonDispatcher;

Then initialize backButtonDispatcher in _MyAppState after delegate.setNewRoutePath(SplashPage);:

backButtonDispatcher = ShoppingBackButtonDispatcher(delegate);

Finally, use this dispatcher in your router in the build method by adding it before the routerDelegate: delegate, statement:

backButtonDispatcher: backButtonDispatcher,

This time, use Hot Restart instead of Hot Reload to restart the app. Then, observe there aren’t changes to the flow of navigation because, as mentioned earlier, the didPopRoute in ShoppingBackButtonDispatcher does nothing special.

Deep Linking

To implement deep links, this tutorial uses the uni_links package. Add it to the pubspec.yaml using the version mentioned here.

This package helps with deep links on Android, as well as Universal Links and Custom URL Schemes on iOS. To handle deep links on Android, modify the AndroidManifest.xml file in the android/app/src/main directory. For Android, add an <intent-filter> tag inside the MainActivity‘s <activity> tag as follows:

  <action android:name="android.intent.action.VIEW" />
  <category android:name="android.intent.category.DEFAULT" />
  <category android:name="android.intent.category.BROWSABLE" />
     android:host="deeplinks" />

For iOS, modify ios/Runner/Info.plist by adding:


Both of these changes add a scheme named navapp to the app where navapp means Navigation App. In terms of deep links, this means this app can open URLs that look like navapp://deeplinks. You can use whatever scheme you like as long as it’s unique.

Now that you know how to make deep links open your app, follow the steps below to understand how to consume the link the user clicked on.

Parse Deep Link URI

Open the router_delegate.dart file and add parseRoute, which takes a Uri as an argument, parses the path and sets the page(s):

void parseRoute(Uri uri) {
// 1
  if (uri.pathSegments.isEmpty) {

// 2
  // Handle navapp://deeplinks/details/#
  if (uri.pathSegments.length == 2) {
    if (uri.pathSegments[0] == 'details') {
// 3
      pushWidget(Details(int.parse(uri.pathSegments[1])), DetailsPageConfig);
  } else if (uri.pathSegments.length == 1) {
    final path = uri.pathSegments[0];
// 4
    switch (path) {
      case 'splash':
      case 'login':
      case 'createAccount':
// 5
          _createPage(Login(), LoginPageConfig),
          _createPage(CreateAccount(), CreateAccountPageConfig)
      case 'listItems':
      case 'cart':
          _createPage(ListItems(), ListItemsPageConfig),
          _createPage(Cart(), CartPageConfig)
      case 'checkout':
          _createPage(ListItems(), ListItemsPageConfig),
          _createPage(Checkout(), CheckoutPageConfig)
      case 'settings':
          _createPage(ListItems(), ListItemsPageConfig),
          _createPage(Settings(), SettingsPageConfig)

In the code above:

  1. Check if there are no pathSegments in the URI. If there are, navigate to the Splash page.
  2. Handle the special case for the Details page, as the path will have two pathSegments.
  3. Parse the item number and push a Details page with the item number. In a real app, this item number could be a product’s unique ID.
  4. Use path as an input for the switch case.
  5. In this case and other cases, push the pages necessary to navigate to the destination using setPath.

Next, to link parseRoute to the root widget’s router, open main.dart and add the following import statement:

import 'package:uni_links/uni_links.dart';

Add the following code after ShoppingBackButtonDispatcher backButtonDispatcher;:

StreamSubscription _linkSubscription;

_linkSubscription is a StreamSubscription for listening to incoming links. Call .cancel() on it to dispose of `dispose()` in _MyAppState.

To do so, replace the following methods in _MyAppState:

void initState() {

void dispose() {

Now add the missing initPlatformState, which is responsible for setting up the listener for the deep links:

// Platform messages are asynchronous, so declare them with the async keyword.
Future<void> initPlatformState() async {
  // Attach a listener to the Uri links stream
// 1
  _linkSubscription = getUriLinksStream().listen((Uri uri) {
    if (!mounted) return;
    setState(() {
// 2
  }, onError: (Object err) {
    print('Got error $err');

Here’s what you do in the code above:

  1. Initialize StreamSubcription by listening for any deep link events.
  2. Have the app’s delegate parse the uri and then navigate using the previously defined parseRoute.

At this point, you’ve implemented deep links, so stop the running instance of your app and re-run it to include these changes. Don’t use hot restart or hot reload because there were changes on the native Android and native iOS side of the project and they aren’t considered by these two tools.

Testing Android URIs

To test your deep links on Android, the easiest way is from the command line.

Open Terminal on macOS or Linux or CMD on Windows and verify that the app navigates to the Settings page using the following adb shell command:

adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "navapp://deeplinks/settings"'

Notice that this adb command includes the app’s deep link scheme navapp and host deeplinks from Android Manifest.xml.

To navigate to the Details page with the index of the item as 1, try the following command:

adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "navapp://deeplinks/details/1"'

To navigate to the Cart page, try the following command:

adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "navapp://deeplinks/cart"'

You’ll see your app navigates to the target page. If that’s not the case, check the logs to see if there are any errors.

Where to Go From Here?

Download the final version of this project using the Download Materials button at the top or bottom of this tutorial.

Congratulations! That was a lot of code, but it should help you whenever you plan to implement your own RouterDelegate with Navigator 2.0.

Check out the following links to learn more about some of the concepts in this tutorial:

We hope you enjoyed this tutorial, and if you have any questions or comments, please join the forum discussion below!

Average Rating


Add a rating for this content

5 ratings

More like this