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. -
ShoppingRouterDelegateuses theChangeNotifiermixin, 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
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 outsideShoppingRouterDelegate. -
PopNavigatorRouterDelegateMixinrequires anavigatorKeyused for retrieving the current navigator of theRouter. - Declare a final
AppStatevariable. - 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.
-
currentConfigurationgets called byRouterwhen it detects route information may have changed. “current” means the topmost page of the app i.e._pages.last. This getter returns configuration of typePageConfigurationas defined on line 1 while creatingRouterDelegate<PageConfiguration>. ThecurrentConfigurationfor thislastpage 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
switchcase on thepageConfig‘sUI_PAGEso you know which page to add. - Uses the recently created private
addPageDatato add the widget andPageConfigurationassociated with the correspondingUI_PAGEfrom theswitchcase.
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:
-
replacemethod: Removes the last page, i.e the top-most page of the app, and replaces it with the new page using the add method -
setPathmethod: Clears the entire navigation stack, i.e. the_pageslist, and adds all the new pages provided as the argument -
replaceAllmethod: CallssetNewRoutePath. You’ll see what this method does in a moment. -
pushmethod: This is like theaddPagemethod, but with a different name to be in sync with Flutter’spushandpopnaming. -
pushWidgetmethod: Allows adding a new widget using the argument of typeWidget. This is what you’ll use for navigating to the Details page. -
addAllmethod: 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. :]