Let's imagine you have downloaded the YouTube mobile app. Your friend decided to share a funny cat video and sent you a link to the video in some messenger. You open that link on your smartphone, and you're almost instantly redirected to YouTube to show the video. You don't need to log in again in the mobile app or search for this video among millions of others on YouTube.
What happens if you don't have YouTube on your smartphone?
The correct answer is that you will be redirected to the AppStore or Google Play, depending on your device's operating system. Then, after downloading the YouTube app, the app should automatically open with the video that your friend sent you, unless, of course, something is broken.
In order to foresee this scenario in your mobile app, deep links come to the rescue.
Short Overview
Deep link is a hyperlink that redirects the user to a specific section of the mobile app or site. A deeplink is a URI (Uniform Resource Identifier).
This functionality optimizes the user experience by minimizing the need for repetitive actions and providing efficient navigation to the intended page with a reduced number of clicks.
Work scheme
When a user taps a link, it triggers a URL-based transition. This action prompts the referrer to interpret details like the operating system and the app type, subsequently directing the user to the precise in-app location or website.
In the event that the application is not installed on the smartphone, the transition will be directed to either Google Play or App Store, depending on the operating system of your mobile device: Android or iOS.
Types
In general, deep links can be divided into two types:
- Basic deep links allow direct access to specific pages or sections within an app, making navigation smooth. If the app is not installed, users are redirected to the app store for download.
- Deferred deep links work even if the app is not installed. Users are directed to the app store for installation and then taken to the desired screen or location within the app.
Examples
twitter://
- URI to launch Twitter mobile appYouTube://
- URI to launch YouTube mobile appfb://profile/33138223345
- URI to launch Facebook mobile app with further redirection to the profile page with code 33138223345
The URI format may vary across operating systems due to their unique handling of deep links. Android devices handle deep links with intents, iOS devices - through the openUrl application method.
Using eBay as an example, we can see the difference in URI formats depending on the operating system:
eBay://launch?itm=360703170135
- URI for iOS app
eBay://item/view?id=360703170135
- URI for Android app
Platform Specifics
Deep links in iOS
iOS supports two types of deep linking:
- Custom URL schemes
The URI structure can accommodate any custom unique scheme and host.
your_scheme://any_host
- Universal Links
These type allows to work only with https-scheme and specified host, permissions (entitlements) and hosted apple-app-site-association file.
https://your_host
To configure your application to work with deep links, you need to add a few lines to Info.plist. Taking Custom URL schemes as an example:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>custom.host</string>
<key>CFBundleURLSchemes</key>
<array>
<string>custom_scheme</string>
</array>
</dict>
</array>
In this example, the deep link is custom_scheme://custom.host
.
Deep links in Android
Android also supports two types of deep link handling, which are similar to their equivalents in iOS:
- Deep links allow working with custom scheme with optional host. This is equivalent to Custom URL schemes in iOS.
- App Links only work with http-scheme and specified host with hosted file. It is similar to Universal Links in iOS.
To configure deep link support in Android, you need to add a few lines to AndroidManifest.xml. Taking Deep links as an example:
<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="custom_scheme"
android:host="my_app" />
</intent-filter>
In this example, the deep link is custom_scheme://my_app
.
Deep Linking in Flutter App
There are several ways to integrate deep links with Flutter. One option involves leveraging Platform channels, while another approach is to utilize third-party packages for this purpose.
Platform channels
You can read more about working with deep links using Platform channels in this article. In short:
- Set up the Platform channels on both Android and iOS sides.
- Prepare your Flutter app to interact with the Platform channels. Write Flutter code that can send and receive data via these channels.
- Implement a mechanism to catch deep links within your Flutter app. This can be done using any state management approach.
- Process the deep link in your Flutter code. Identify the paths contained in the deep link, and check if your application supports these paths.
- Handle path with named routes in Navigator 1.0 (routes parameter or
onGenerateRoute
) or Router widget in Navigator 2.0 (recommended way, more declarative). You can read about Navigator 2.0 handling here.
To test deep linking on Android enter this command in a terminal or command line while emulator running:
adb shell am start -a android.intent.action.VIEW \
-c android.intent.category.BROWSABLE \
-d "scheme://myApp/page1/1"
If you test this on iOS, enter the command in the terminal while simulator running:
xcrun simctl openurl booted customscheme://ourApp/page/2
Plugins
There are several plugins for working with deep links. This article covers uni_links
and firebase_dynamic_links
.
uni_links
The most popular plugin in pub.dev for working with deep links. You do not need to write Platform channels to work with deep links. You just need to import this plugin, catch the deep links with the methods it provides and process them with Navigator 1.0 or 2.0.
Advantages of this plugin:
- Stable code base.
- Constantly updated.
- Clear documentation.
- Lots of online resources on how to work with this plugin.
The key disadvantage is lack of built-in functionality for redirecting users to app stores.
firebase_dynamic_links
This plugin is the most functional for working with deep links in Flutter, it provides the ability to catch deep links both while the app is running, and in the background, and while the app is closed. If the app isn't installed, the deep link will navigate the user to the appropriate app store to download it. If the app is already installed, it will just open the app to the designated content.
Configuring Firebase and platforms
To use Firebase Dynamic Links, it is necessary to follow these steps to set up Firebase:
- Sign in to your Firebase account and select your project.
- On the left side menu, click on "Engage" to expand it, then select "Dynamic Links".
- Click on the "New Dynamic Link" button.
- Add URL prefix - the beginning of your URI, when you click on it the application will open.
There are two options for URL prefixes: you can use your custom domain or a pre-created Firebase domain that ends with .page.link.
If you use a custom domain, you need to confirm that you are its owner.
- Next, you can create a deep link in Firebase and use it in the application. You can also create a deep link in the code, which will be described later in the article.
Configuring Android to work with firebase_dynamic_links
does not require anything specific, just add a few lines in the AndoidManifest.xml as described above.
iOS setup requires an Apple developer account to integrate firebase_dynamic_links
. Follow the steps below to set up Firebase and Xcode:
- In the Firebase console, open Project settings.
- Register App Store ID (possibly not yet existing) and Team ID.
Team ID can be found in the Membership tab on the website.
- On the Apple Developer website, a provisioning profile must be created for your application, the Associated Domain option must be enabled.
- The next step is to open the ios folder with Xcode. Navigate to the Signing and Capabilities tab. Make sure that the Provisioning Profile and Team are correct. Add your URL prefix in the Associated Domains field, with applinks:.
- In the Info → URL Types tab, add a new URL type. The URL Schemes field must contain the bundle id of your application, in the Identifier field you can write the Bundle ID or something else.
- To verify that you have done everything correctly, you can insert a link like this in the search bar of your browser:
https://yourApp.page.link/apple-app-site-association
. The following Json should be displayed:
{
"applinks":{
"apps":[],
"details":[
{
"appID":"Q8K34ZB576.com.sheepapps.reely",
"paths":[
"NOT /_/*",
"/*"
]
}
]
}
}
- The next step is to add a few lines to Info.plist.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FirebaseDynamicLinksCustomDomains</key>
<array>
<string>https://custom.domain.io/path1</string>
<string>https://custom.domain.io/path2</string>
</array>
...other settings
</dict>
</plist>
For more details on configuring firebase_dynamic_links
, you can refer to the official Firebase documentation, or check out this helpful Medium article.
Using firebase_dynamic_links
Deep link can be created by two methods:
FirebaseDynamicLinks.buildLink
FirebaseDynamicLinks.buildShortLink
buildLink
creates a long link that is hard for the user to read, while buildShortLink
creates a short link like https://app.page.link/34566677
.
Let's create a deep link like https://ourApp.com/page/?id=55
.
static const String _androidPackageName = 'com.sheepapps.app';
static const String _iOSBundleId = 'com.sheepapps.app';
static const String _appStoreId = '1611717111';
static const String _uriPrefix = 'https://sheepapps.page.link'; //prefix in Firebase
static const String _deepLink = 'https://ourApp.com/page';
static const int _minimumVersion = 1;
static Future<Uri> createDeepLink({required String id}) async {
final parameters = DynamicLinkParameters(
uriPrefix: _uriPrefix,
link: Uri.parse('$_deepLink/?id=$id'),
androidParameters: const AndroidParameters(
packageName: _androidPackageName,
minimumVersion: _minimumVersion,
),
iosParameters: IOSParameters(
bundleId: _iOSBundleId,
minimumVersion: _minimumVersion.toString(),
appStoreId: _appStoreId,
),
);
final shortDynamicLink = await FirebaseDynamicLinks.instance.buildShortLink(parameters);
final uri = shortDynamicLink.shortUrl;
return uri;
}
If you want an application running in the background to be able to catch deep links, you need to subscribe to the stream onLink
provided by Firebase.
static void initDeepLinks() {
FirebaseDynamicLinks.instance.onLink.listen((deepLinkData) {
final deepLink = deepLinkData.link;
log('Link received: $deepLink');
var id = '';
if (deepLink.queryParameters.containsKey('id')) {
id = deepLink.queryParameters['id']!;
}
log('id from deep link: $id');
}).onError((error) {
log('onLink error:');
log(error.message);
});
}
This method can be called in initState
above the widget hierarchy.
If a user clicks a deep link while the app is fully closed (both in the foreground and background), use the FirebaseDynamicLinks.getInitialLink
method to open and navigate to the content associated with the deep link.
Let's code a method that parses the deep link.
static Future<String?> retrieveDeepLinkId() async {
String? id;
try {
final initialLink = await FirebaseDynamicLinks.instance.getInitialLink();
final deepLink = initialLink?.link;
if (deepLink != null) {
if (deepLink.queryParameters.containsKey('id')) {
id = deepLink.queryParameters['id'];
}
}
} catch (e) {
log(e.toString());
}
return id;
}
It is best to call this method in main
function and pass the required parameter in the constructor.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final id = await retrieveDeepLinkId();
runApp(App(id: id));
}
The methods mentioned above can be conveniently organized into an abstract static class, a singleton class, or any other appropriate class structure.
Testing firebase_dynamic_links
For testing the functionality of firebase_dynamic_links
on iOS, it's necessary to use real device. This is due to the requirement of operating in a profile or release mode. On Android, deep links can be tested on an emulator. However, it's crucial to note that testing should be performed outside the debug mode.
To test an application launch via a deep link, you can create the deep link programmatically or in Firebase, and paste it into the emulator's search bar. This should open the application, or if the app isn't installed, Google Play/App Store should open. To test deep links while the app is in the background, keep the application in recent apps and follow the above steps.
To learn more about firebase_dynamic_links
, check the Flutter-specific guide and the general Firebase documentation.
Summary
Deep linking provides a significant advantage by navigating users directly to specific sections of an app, enhancing their user experience by reducing redundant steps. Both iOS and Android have unique specifications when dealing with deep links, so a solid understanding of these specifics is key.
Flutter offers various ways to integrate deep linking, including the use of Platform channels and several third-party packages. Notably, firebase_dynamic_links
provides a wide range of functionalities. It can catch deep links while the app is running, in the background, or even when the app is closed. If the app isn't installed, firebase_dynamic_links
will guide the user to the appropriate app store for download. Setting up Firebase Dynamic Links requires following some critical steps, like choosing URL prefixes and confirming domain ownership.
For scenarios where redirection to the app store isn't necessary, uni_links
can be a suitable choice.
All these tools, when properly leveraged, can help optimize navigation in your Flutter app, thus boosting user engagement and experience.