Home Flutter Books Flutter Apprentice

8
Deep Links & Web URLs Written by Vincent Ngo

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Sometimes, opening your app and working through the navigation to get to a screen is just too much trouble for the user. Redirecting to a specific part of your app is a powerful marketing tool for user engagement. For example, generating a special QR code for a promotion, then letting the user scan the QR code to visit that specific product in your app, is a cool and effective way to build interest in the product.

In the last chapter, you learned how to use Navigator 2.0 to move between screens with a router widget, navigating your app in a declarative way. Now, you’ll learn to use more features of Navigator 2.0. Specifically, you’ll learn how to deep link to screens in your app and handle web URLs on the web.

For example, here’s how Fooderlich will look in the Chrome web browser:

By the end of this chapter, you’ll know how to:

  • Parse URL strings and query parameters.
  • Convert a URL to and from your app state.
  • Support deep linking on iOS and Android.
  • Support URL-driven navigation in the browser for Flutter web apps.

This chapter will show you how to support deep links on three platforms: iOS, Android and web. You’ll be able to direct users to any screen of your choice.

Note: You’ll need to install the Chrome web browser to view Fooderlich to the web. If you don’t have Chrome already, you can get it from https://www.google.com/chrome/. The Flutter web project can run on other browsers, but this chapter only covers testing and development on Chrome.

Understanding deep links

A deep link is a URL that navigates to a specific destination in your mobile app. You can think of deep links like a URL address you enter into a web browser to go to a specific page of a website rather than the home page.

Deep links help with user engagement and business marketing. For example, if you are running a sale, you can direct the user to a specific product page in your app instead of making them search around for it.

Imagine that Fooderlich has its own website. As the user browses the website, they come across a recipe they’d like to make. By using deep linking, you could let users click on the recipe to open the app directly on the Grocery Item screen and immediately start adding ingredients to their shopping list. This saves them time and makes the app more enjoyable.

  • With deep linking, Fooderlich is more automated. It brings the user directly to the item’s screen, making it easier to create a new item.
  • Without deep linking, it’s more manual. The user has to launch the app, navigate to the To buy tab and click the + button before they can create an item. That takes three steps instead of one, and likely some head-scratching too!

Types of deep links

There are three types of deep links:

Getting started

Note: We recommend that you use the starter project for this chapter rather than continuing with the project from the last chapter.

Project files

Before you dive into parsing URLs, check out the new files in this starter project.

Screens folder

There’s one change in lib/screens/:

Models folder

There’s two new additions in lib/models/:

New packages

There are two new packages in pubspec.yaml:

url_launcher: ^6.0.4
shared_preferences: ^2.0.5

New Flutter web project

The starter project includes a pre-built Flutter web project.

Setting up deep links

To enable deep linking on iOS and Android, you have to add some metadata tags in the respective platforms.

Setting up deep links on iOS

Open ios/Runner/Info.plist. You’ll see some new key-value pairs, which enable deep linking for iOS:

<key>FlutterDeepLinkingEnabled</key>
<true/>
<key>CFBundleURLTypes</key>
<array>
  <dict>
  <key>CFBundleTypeRole</key>
  <string>Editor</string>
  <key>CFBundleURLName</key>
  <string>raywenderlich.com</string>
  <key>CFBundleURLSchemes</key>
  <array>
  <string>fooderlich</string>
  </array>
  </dict>
</array>

Setting up deep links on Android

Open android/app/src/main/AndroidManifest.xml. Here you’ll also find two new definitions in the <data> tag:

<!-- Deep linking -->
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
  android:scheme="fooderlich"
  android:host="raywenderlich.com" />
</intent-filter>
fooderlich://raywenderlich.com/<path>

Overview of Fooderlich’s paths

You have many options when it comes to which of Fooderlich’s various screens you can deep link to. Here are all the possible paths you can redirect your users to:

Path: /

The app initializes and checks the app cache to see if the user is logged in and has completed the onboarding guide.

Path: /home?tab=[index]

The /home path redirects to the Home screen only if the user has logged in and completed onboarding. It contains one query parameter, tab, which directs to a tab index. As shown in the screenshots below, the tab index is 0, 1 or 2 respectively.

Path: /profile

If the user has logged in and completed onboarding, /profile will redirect to the Profile screen.

Path: /item?id=[uuid]

/item redirects to the Grocery Item screen. It contains one query parameter, id. There are two scenarios:

fooderlich://raywenderlich.com/<path>
http://localhost:60738/#/<path>

Recapping Navigator 2.0

In the last chapter, you learned how to set up four components: RouterDelegate, Router, Navigator and BackButtonDispatcher.

Deep links under the hood

For deep links to work, you need to do two key things: convert a URL to an app state and convert an app state to a URL. Next, you’ll see both in detail.

Converting a URL to an app state

The first part of supporting deep links is to figure out which state of the app corresponds to a specific URL. Here’s how the conversion happens:

Converting the app state to a URL string

When the user taps a button or the app state changes, you need to change the current URL. Here’s what happens when you set up your app to handle URLs:

Creating a navigation state object

AppLink is the intermediary object between a URL string and your app state. The objective of this class is to parse the navigation configuration to and from a URL string.

class AppLink {
  // 1
  static const String kHomePath = '/home';
  static const String kOnboardingPath = '/onboarding';
  static const String kLoginPath = '/login';
  static const String kProfilePath = '/profile';
  static const String kItemPath = '/item';
	// 2
  static const String kTabParam = 'tab';
  static const String kIdParam = 'id';
	// 3
  String location;
  // 4
  int currentTab;
  // 5
  String itemId;
  // 6
  AppLink({this.location, this.currentTab, this.itemId});

  // TODO: Add fromLocation

  // TODO: Add toLocation
}

Converting a URL string to an AppLink

AppLink is an object that helps store the route information. It helps to parse the URL string to a route and vice versa, converting the route information back to a URL string. It essentially encapsulates all the logic that transforms a simple string into a state and back.

static AppLink fromLocation(String location) {
  // 1
  location = Uri.decodeFull(location);
  // 2
  final uri = Uri.parse(location);
  final params = uri.queryParameters;
  // 3
  void trySet(String key, void Function(String) setter) {
    if (params.containsKey(key)) setter?.call(params[key]);
  }
  // 4
  final link = AppLink()..location = uri.path;
  // 5
  trySet(AppLink.kTabParam, (s) => link.currentTab = int.tryParse(s));
  trySet(AppLink.kIdParam, (s) => link.itemId = s);
  // 6
  return link;
}

Converting an AppLink to a URL string

The app will also need the converse transformation, from AppLink to simple string.

String toLocation() {
  // 1
  String addKeyValPair({String key, String value}) =>
      value == null ? '' : '${key}=$value&';
	// 2
  switch (location) {
    // 3
    case kLoginPath:
     return kLoginPath;
    // 4
    case kOnboardingPath:
      return kOnboardingPath;
    // 5
    case kProfilePath:
      return kProfilePath;
    // 6
    case kItemPath:
      var loc = '$kItemPath?';
      loc += addKeyValPair(key: kIdParam, value: itemId);
      return Uri.encodeFull(loc);
    // 7
    default:
      var loc = '$kHomePath?';
      loc += addKeyValPair(key: kTabParam, value: currentTab.toString());
      return Uri.encodeFull(loc);
  }
}

Creating a route information parser

In the navigation directory, create a new file called app_route_parser.dart and add the following:

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

// 1
class AppRouteParser extends RouteInformationParser<AppLink> {
  // 2
  @override
  Future<AppLink> parseRouteInformation(
      RouteInformation routeInformation) async {
    // 3
    final link = AppLink.fromLocation(routeInformation.location);
    return link;
  }

  // 4
  @override
  RouteInformation restoreRouteInformation(AppLink appLink) {
    // 5
    final location = appLink.toLocation();
    // 6
    return RouteInformation(location: location);
  }
}

Connecting the parser to the app router

Now that you’ve set up your RouteInformationParser, it’s time to connect it to your router delegate.

final routeParser = AppRouteParser();
import 'navigation/app_route_parser.dart';
return MaterialApp.router(
  theme: theme,
  title: 'Fooderlich',
  backButtonDispatcher: RootBackButtonDispatcher(),
  // 1
  routeInformationParser: routeParser,
  // 2
  routerDelegate: _appRouter,
);

Converting a URL to an app state

When the user enters a new URL on the web or triggers a deep link on mobile, RouteInformationProvider notifies RouteInformationParser that there’s a new route, as shown below:

Configuring navigation

Quick theory test: Where’s the logic that maps a specific URL path to a specific screen? It’s in setNewRoutePath()!

<AppLink>
import 'app_link.dart';
// 1
@override
Future<void> setNewRoutePath(AppLink newLink) async {
  // 2
  switch (newLink.location) {
    // 3
    case AppLink.kProfilePath:
      profileManager.tapOnProfile(true);
      break;
    // 4
    case AppLink.kItemPath:
      // 5
      if (newLink.itemId != null) {
        groceryManager.setSelectedGroceryItem(newLink.itemId);
      } else {
        // 6
        groceryManager.createNewItem();
      }
      // 7
      profileManager.tapOnProfile(false);
      break;
    // 8
    case AppLink.kHomePath:
      // 9
      appStateManager.goToTab(newLink.currentTab ?? 0);
      // 10
      profileManager.tapOnProfile(false);
      groceryManager.groceryItemTapped(null);
      break;
    // 11
    default:
      break;
  }
}

Converting the app state to a URL

At this point, you’ve converted a URL to an app state. Next, you need to do the opposite. When the user taps a button or navigates to another screen, you need to convert the app state back to a URL string. For the web app, this will synchronize the browser’s address bar.

AppLink getCurrentPath() {
  // 1
  if (!appStateManager.isLoggedIn) {
    return AppLink(location: AppLink.kLoginPath);
  // 2
  } else if (!appStateManager.isOnboardingComplete) {
    return AppLink(location: AppLink.kOnboardingPath);
  // 3
  } else if (profileManager.didSelectUser) {
    return AppLink(location: AppLink.kProfilePath);
  // 4
  } else if (groceryManager.isCreatingNewItem) {
    return AppLink(location: AppLink.kItemPath);
  // 5
  } else if (groceryManager.selectedGroceryItem != null) {
    final id = groceryManager.selectedGroceryItem.id;
    return AppLink(location: AppLink.kItemPath, itemId: id);
  // 6
  } else {
    return AppLink(
        location: AppLink.kHomePath,
        currentTab: appStateManager.getSelectedTab);
  }
}
@override
AppLink get currentConfiguration => getCurrentPath();

Testing deep links

For your next step, you’ll test how deep linking works on iOS, Android and the web.

Testing deep links on iOS

In Android Studio, select an iOS device and press the Play button:

Deep linking to the Home screen

Enter the following in your terminal:

xcrun simctl openurl booted 'fooderlich://raywenderlich.com/home?tab=1'

Deep linking to the Profile screen

Next, run the following command:

xcrun simctl openurl booted 'fooderlich://raywenderlich.com/profile'

Deep linking to create a new item

Next, run the following command:

xcrun simctl openurl booted 'fooderlich://raywenderlich.com/item'

Resetting the cache in the iOS simulator

Recall that AppStateManager checks with AppCache to see whether the user is logged in or has onboarded. If you want to reset the cache to see the Login screen again, you have two options:

Testing deep links on Android

Stop running on iOS. Open Android Studio, select an Android device and click the Play button:

Deep linking to the Home screen

Enter the following in your terminal:

~/Library/Android/sdk/platform-tools/adb shell am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d 'fooderlich://raywenderlich.com/home?tab=1'

Deep linking to the Profile screen

Next, run the following command:

~/Library/Android/sdk/platform-tools/adb shell am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d 'fooderlich://raywenderlich.com/profile'

Deep linking to Create New Item

Next, run the following command:

~/Library/Android/sdk/platform-tools/adb shell am start -a android.intent.action.VIEW \
    -c android.intent.category.BROWSABLE \
    -d 'fooderlich://raywenderlich.com/item'

Resetting the cache in Android

If you need to reset your user cache, here’s what you do:

Running the web app

Stop running on Android. In Android Studio, select Chrome (web) and click the Play button:

Key points

  • The app notifies RouteInformationProvider when there’s a new route.
  • The provider passes the route information to RouteInformationParser to parse the URL string.
  • The parser converts app state to and from a URL string.
  • AppLink models the navigation state. It is a user-defined data type that encapsulates information about a URL string.
  • In development mode, the Flutter web app does not persist data between app launches. The web app generated in release mode will work on the other browsers.

Where to go from here?

If you’re curious about how to remove the # symbol from a URL in the web app, check out: https://flutter.dev/docs/development/ui/navigation/url-strategies.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.