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. By Kevin D Moore.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Flutter Navigator 2.0 and Deep Links
40 mins
- Getting Started
- Navigator 1.0
- Navigator 2.0
- Pages Overview
- Login Page
- Create Account Page
- Shopping List Page
- Details Page
- Cart Page
- Checkout Page
- Settings Page
- Pages Setup
- AppState
- RouterDelegate
- Implementing build
- Removing Pages
- Creating and Adding a Page
- Modifying the Contents
- RouteInformationParser
- Root Widget and Router
- Navigating Between Pages
- Splash Page Navigation
- BackButtonDispatcher
- Deep Linking
- Parse Deep Link URI
- Testing Android URIs
- Where to Go From Here?
RouterDelegate
RouterDelegate contains the core logic for Navigator 2.0. This includes controlling the navigation between pages. This class is an abstract class that requires classes that extend RouterDelegate
to implement all of its unimplemented methods.
Begin by creating a new Dart file in the router directory called router_delegate.dart. You will name the RouterDelegate
for this app ShoppingRouterDelegate
. Add the following import statements:
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import '../app_state.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 UI pages. 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
@override
final GlobalKey<NavigatorState> navigatorKey;
// 5
final AppState appState;
// 6
ShoppingRouterDelegate(this.appState) : navigatorKey = GlobalKey() {
appState.addListener(() {
notifyListeners();
});
}
// 7
/// Getter for a list that cannot be changed
List<MaterialPage> get pages => List.unmodifiable(_pages);
/// Number of pages function
int numPages() => _pages.length;
// 8
@override
PageConfiguration get currentConfiguration =>
_pages.last.arguments as PageConfiguration;
}
Ignore the errors for now, you will resolve them soon. Here’s what’s happening in the code above:
- This represents the app’s
RouterDelegate
,ShoppingRouterDelegate
. It extends the abstractRouterDelegate
, which produces a configuration for eachRoute
. This configuration isPageConfiguration
. -
ShoppingRouterDelegate
uses theChangeNotifier
mixin, which helps notify any listeners of this delegate to update themselves whenevernotifyListeners()
is invoked. This class also usesPopNavigatorRouterDelegateMixin
, which lets you remove pages. It’ll also be useful later when you implementBackButtonDispatcher
. - This list of
Page
s 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 outsideShoppingRouterDelegate
. -
PopNavigatorRouterDelegateMixin
requires anavigatorKey
used for retrieving the current navigator of theRouter
. - Declare a final
AppState
variable. - Define the constructor. This constructor takes in the current app state and creates a global navigator key. It’s important that you only create this key once.
- Define public getter functions.
-
currentConfiguration
gets called byRouter
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 typePageConfiguration
as defined on line 1 while creatingRouterDelegate<PageConfiguration>
. ThecurrentConfiguration
for thislast
page can be accessed as_pages.last.arguments
.
Next, you’ll implement 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:
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onPopPage: _onPopPage,
pages: buildPages(),
);
}
Navigator
uses the previously defined navigatorKey
as its key. Navigator
needs to know what to do when the app requests the removal or popping of a page via a back button press and calls _onPopPage
.
pages calls buildPages
to return the current list of pages, which represents the app’s navigation stack.
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
if (canPop()) {
pop();
return true;
} else {
return false;
}
}
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:
- There’s a request to pop the route. If the route can’t handle it internally, it returns
false
. - Otherwise, check to see if we can remove the top page and remove the page from the list of pages.
Note that route.settings
extends RouteSettings
.
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) {
_pages.remove(page);
}
}
_removePage
is a private method, so to access it from anywhere in the app, use RouterDelegate‘s popRoute
method. Now add pop methods:
void pop() {
if (canPop()) {
_removePage(_pages.last);
}
}
bool canPop() {
return _pages.length > 1;
}
@override
Future<bool> popRoute() {
if (canPop()) {
_removePage(_pages.last);
return Future.value(true);
}
return Future.value(false);
}
These methods ensure there are at least two pages in the list. Both pop
and popRoute
will call _removePage
to remove a page and return true
if it can pop, ottherwise, return false
to close the app. If you didn’t add the check here and called _removePage
on the last page of the app, you would see a blank screen.
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 first argument for this method is 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) {
_pages.add(
_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 !=
pageConfig.uiPage;
if (shouldAddPage) {
// 2
switch (pageConfig.uiPage) {
case Pages.Splash:
// 3
_addPageData(Splash(), SplashPageConfig);
break;
case Pages.Login:
_addPageData(Login(), LoginPageConfig);
break;
case Pages.CreateAccount:
_addPageData(CreateAccount(), CreateAccountPageConfig);
break;
case Pages.List:
_addPageData(ListItems(), ListItemsPageConfig);
break;
case Pages.Cart:
_addPageData(Cart(), CartPageConfig);
break;
case Pages.Checkout:
_addPageData(Checkout(), CheckoutPageConfig);
break;
case Pages.Settings:
_addPageData(Settings(), SettingsPageConfig);
break;
case Pages.Details:
if (pageConfig.currentPageAction != null) {
_addPageData(pageConfig.currentPageAction.widget, pageConfig);
}
break;
default:
break;
}
}
}
The code above does the following:
- 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.
- Uses a
switch
case on thepageConfig
‘sUI_PAGE
so you know which page to add. - Uses the recently created private
addPageData
to add the widget andPageConfiguration
associated with the correspondingUI_PAGE
from theswitch
case.
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.
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) {
_pages.removeLast();
}
addPage(newRoute);
}
// 2
void setPath(List<MaterialPage> path) {
_pages.clear();
_pages.addAll(path);
}
// 3
void replaceAll(PageConfiguration newRoute) {
setNewRoutePath(newRoute);
}
// 4
void push(PageConfiguration newRoute) {
addPage(newRoute);
}
// 5
void pushWidget(Widget child, PageConfiguration newRoute) {
_addPageData(child, newRoute);
}
// 6
void addAll(List<PageConfiguration> routes) {
_pages.clear();
routes.forEach((route) {
addPage(route);
});
}
Here’s a breakdown of the code above:
-
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 -
setPath
method: Clears the entire navigation stack, i.e. the_pages
list, and adds all the new pages provided as the argument -
replaceAll
method: CallssetNewRoutePath
. You’ll see what this method does in a moment. -
push
method: This is like theaddPage
method, but with a different name to be in sync with Flutter’spush
andpop
naming. -
pushWidget
method: Allows adding a new widget using the argument of typeWidget
. This is what you’ll use for navigating to the Details page. -
addAll
method: Adds a list of pages.
The last overridden method of the RouterDelegate
is 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:
@override
Future<void> setNewRoutePath(PageConfiguration configuration) {
final shouldAddPage = _pages.isEmpty ||
(_pages.last.arguments as PageConfiguration).uiPage !=
configuration.uiPage;
if (shouldAddPage) {
_pages.clear();
addPage(configuration);
}
return SynchronousFuture(null);
}
When an page action is requested, you want to record the action associated with the page. The _setPageAction method will do that. Add:
void _setPageAction(PageAction action) {
switch (action.page.uiPage) {
case Pages.Splash:
SplashPageConfig.currentPageAction = action;
break;
case Pages.Login:
LoginPageConfig.currentPageAction = action;
break;
case Pages.CreateAccount:
CreateAccountPageConfig.currentPageAction = action;
break;
case Pages.List:
ListItemsPageConfig.currentPageAction = action;
break;
case Pages.Cart:
CartPageConfig.currentPageAction = action;
break;
case Pages.Checkout:
CheckoutPageConfig.currentPageAction = action;
break;
case Pages.Settings:
SettingsPageConfig.currentPageAction = action;
break;
case Pages.Details:
DetailsPageConfig.currentPageAction = action;
break;
default:
break;
}
}
Now comes the most important method, buildPages
. This method will return a list of pages based on the current app state:
List<Page> buildPages() {
// 1
if (!appState.splashFinished) {
replaceAll(SplashPageConfig);
} else {
// 2
switch (appState.currentAction.state) {
// 3
case PageState.none:
break;
case PageState.addPage:
// 4
_setPageAction(appState.currentAction);
addPage(appState.currentAction.page);
break;
case PageState.pop:
// 5
pop();
break;
case PageState.replace:
// 6
_setPageAction(appState.currentAction);
replace(appState.currentAction.page);
break;
case PageState.replaceAll:
// 7
_setPageAction(appState.currentAction);
replaceAll(appState.currentAction.page);
break;
case PageState.addWidget:
// 8
_setPageAction(appState.currentAction);
pushWidget(appState.currentAction.widget, appState.currentAction.page);
break;
case PageState.addAll:
// 9
addAll(appState.currentAction.pages);
break;
}
}
// 10
appState.resetCurrentAction();
return List.of(_pages);
}
- If the splash screen hasn’t finished, just show the splash screen.
- Switch on the current action state.
- If there is no action, do nothing.
- Add a new page, given by the action’s page variable.
- Pop the top-most page.
- Replace the current page.
- Replace all of the pages with this page.
- Push a widget onto the stack (Details page)
- Add a list of pages.
- Reset the page state to none.
RouterDelegate
is a lot to take in. Take a break to digest what you just learned. :]