Deep links are a powerful mechanism in mobile applications that allow users to navigate directly to a specific screen or content within the app from an external source, such as a web page, email, or another application. In Flutter, effectively handling deep links is essential for creating a seamless user experience, improving user engagement, and facilitating app discoverability. This article explores different types of deep links, their implementation, and best practices for handling them in Flutter apps.
What are Deep Links?
Deep links are URIs (Uniform Resource Identifiers) that route users to specific content within a mobile app. When a user clicks on a deep link, the operating system recognizes it and, if the app is installed, opens the app and directs the user to the designated content or screen. If the app is not installed, the user can be redirected to the app store or a fallback URL.
Types of Deep Links
There are primarily two types of deep links:
- Custom URL Schemes: These use a custom URI scheme (e.g.,
myapp://) and are simpler to implement but less robust due to potential conflicts with other apps using the same scheme. - Universal Links (Android App Links and iOS Universal Links): These use standard HTTP/HTTPS URLs, associating your website domain with your app. This approach is more secure and reliable as it verifies the app’s association with the domain.
Custom URL Schemes
Custom URL schemes are easy to set up but have certain drawbacks. Here’s how to implement them in Flutter:
Step 1: Configure the Native Platforms
First, configure the custom URL scheme in your native Android and iOS projects.
Android (AndroidManifest.xml):
<application
android:label="MyApp">
<activity
android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<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="myapp" android:host="open" />
</intent-filter>
</activity>
</application>
</manifest>
In the AndroidManifest.xml file, within the activity tag for your main activity, add an intent-filter that specifies the custom URL scheme (myapp) and the host (open). This filter tells Android to open the app when a URL matching myapp://open is clicked.
iOS (Info.plist):
<dict>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
</dict>
</array>
</dict>
In the Info.plist file, add a CFBundleURLTypes array with a dictionary that includes CFBundleURLSchemes. Set the array value to your custom URL scheme (myapp). This configuration tells iOS to associate your app with the specified URL scheme.
Step 2: Handle Deep Links in Flutter
Use the uni_links package to listen for and handle deep links in your Flutter app.
dependencies:
flutter:
sdk: flutter
uni_links: ^0.5.1
Install the package using:
flutter pub add uni_links
Implement the deep link handling logic in your Flutter code:
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
String? _latestLink = 'Unknown';
String? _latestUri;
@override
void initState() {
super.initState();
_initDeepLinks();
}
Future _initDeepLinks() async {
// Attach a listener to the stream
_handleIncomingLinks();
// Get the latest link
_handleInitialUri();
}
/// Handle incoming links - the ones that the app will receive from the OS
/// while already started.
void _handleIncomingLinks() {
uriLinkStream.listen((Uri? uri) {
if (!mounted) return;
print('Received URI: $uri');
setState(() {
_latestUri = uri?.toString() ?? 'Unknown';
_latestLink = uri?.pathSegments.join("/");
});
}, onError: (Object err) {
if (!mounted) return;
print('Got err: $err');
setState(() {
_latestUri = 'Failed to get latest link: $err.';
_latestLink = 'Unknown';
});
});
}
/// Handle initial URI - the one the app was started with
///
/// **Note**: it is different from the incoming links stream
Future _handleInitialUri() async {
// In this example app this is an almost useless case, but it is nonetheles
// a good sample on how to handle this event.
try {
final uri = await getInitialUri();
if (uri == null) {
print('No initial URI received');
} else {
print('Initial URI received: $uri');
}
if (!mounted) return;
setState(() {
_latestUri = uri?.toString() ?? 'Unknown';
_latestLink = uri?.pathSegments.join("/");
});
} on PlatformException {
// Platform messages may fail, so we use a try/catch PlatformException.
// Handle exception by warning the user, and start a fallback workflow.
print('Failed to get initial URI.');
setState(() {
_latestUri = 'Failed to get initial URI.';
_latestLink = 'Unknown';
});
} on FormatException catch (err) {
if (!mounted) return;
print('Malformed Initial URI received: $err');
setState(() {
_latestUri = 'Malformed Initial URI received: $err';
_latestLink = 'Unknown';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Deep Linking in Flutter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Latest URI: $_latestUri.n'),
Text('Latest Link: $_latestLink.n'),
],
),
),
),
);
}
}
Explanation:
- Import
uni_links: Import the necessary packages. - Initialize Deep Links: In the
initStatemethod, call_initDeepLinks()to set up the deep link handling. - Handle Incoming Links: The
_handleIncomingLinks()method listens for incoming deep links while the app is running. It updates the UI with the received URI. - Handle Initial URI: The
_handleInitialUri()method retrieves the initial URI when the app is launched from a deep link. - Update UI: The UI is updated with the latest URI and link information.
Universal Links
Universal Links are a more secure and reliable way to handle deep linking. They associate your app with a website domain, ensuring that only your app can handle links from that domain.
Step 1: Configure the Native Platforms
Android App Links:
- Associate Your App with Your Website:
- Create a Digital Asset Links File:
In your AndroidManifest.xml file, add the android:autoVerify="true" attribute to the intent-filter. This enables Android to verify the link.
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="yourdomain.com" />
</intent-filter>
Create a assetlinks.json file and host it at https://yourdomain.com/.well-known/assetlinks.json.
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints":
["YOUR_SHA256_CERT_FINGERPRINT"]
}
}
]
Replace com.example.myapp with your app’s package name and YOUR_SHA256_CERT_FINGERPRINT with your app’s SHA256 certificate fingerprint.
iOS Universal Links:
- Associate Your App with Your Website:
- Enable Associated Domains Entitlement:
Create an apple-app-site-association file and host it at https://yourdomain.com/.well-known/apple-app-site-association.
{
"applinks": {
"apps": [],
"details": [
{
"appID": "YOUR_TEAM_ID.com.example.myapp",
"paths": ["/open/*"]
}
]
}
}
Replace YOUR_TEAM_ID.com.example.myapp with your app’s Team ID and Bundle Identifier. The paths array specifies the URL paths that should open in your app.
In your Xcode project, enable the Associated Domains entitlement and add your domain:
applinks:yourdomain.com
Step 2: Handle Universal Links in Flutter
Use the uni_links package (same as with Custom URL Schemes) to handle Universal Links in your Flutter app.
dependencies:
flutter:
sdk: flutter
uni_links: ^0.5.1
The Flutter code to handle the deep links is identical to the Custom URL Schemes example. The uni_links package automatically detects and handles both types of deep links.
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
String? _latestLink = 'Unknown';
String? _latestUri;
@override
void initState() {
super.initState();
_initDeepLinks();
}
Future _initDeepLinks() async {
// Attach a listener to the stream
_handleIncomingLinks();
// Get the latest link
_handleInitialUri();
}
/// Handle incoming links - the ones that the app will receive from the OS
/// while already started.
void _handleIncomingLinks() {
uriLinkStream.listen((Uri? uri) {
if (!mounted) return;
print('Received URI: $uri');
setState(() {
_latestUri = uri?.toString() ?? 'Unknown';
_latestLink = uri?.pathSegments.join("/");
});
}, onError: (Object err) {
if (!mounted) return;
print('Got err: $err');
setState(() {
_latestUri = 'Failed to get latest link: $err.';
_latestLink = 'Unknown';
});
});
}
/// Handle initial URI - the one the app was started with
///
/// **Note**: it is different from the incoming links stream
Future _handleInitialUri() async {
// In this example app this is an almost useless case, but it is nonetheles
// a good sample on how to handle this event.
try {
final uri = await getInitialUri();
if (uri == null) {
print('No initial URI received');
} else {
print('Initial URI received: $uri');
}
if (!mounted) return;
setState(() {
_latestUri = uri?.toString() ?? 'Unknown';
_latestLink = uri?.pathSegments.join("/");
});
} on PlatformException {
// Platform messages may fail, so we use a try/catch PlatformException.
// Handle exception by warning the user, and start a fallback workflow.
print('Failed to get initial URI.');
setState(() {
_latestUri = 'Failed to get initial URI.';
_latestLink = 'Unknown';
});
} on FormatException catch (err) {
if (!mounted) return;
print('Malformed Initial URI received: $err');
setState(() {
_latestUri = 'Malformed Initial URI received: $err';
_latestLink = 'Unknown';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Deep Linking in Flutter'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Latest URI: $_latestUri.n'),
Text('Latest Link: $_latestLink.n'),
],
),
),
),
);
}
}
Best Practices for Handling Deep Links
- Implement Fallback URLs:
- Handle Errors Gracefully:
- Use Universal Links:
- Test Thoroughly:
- Deferred Deep Linking:
Provide fallback URLs for cases where the app is not installed, redirecting users to the app store or a relevant web page.
Implement error handling to manage cases where the deep link is malformed or the app fails to navigate to the specified content.
Prefer Universal Links over Custom URL Schemes for better security and reliability.
Test deep links on both Android and iOS devices to ensure they function correctly across different scenarios.
Consider using deferred deep linking (e.g., with Firebase Dynamic Links) to handle cases where the app is installed after the user clicks the link. Deferred deep linking ensures that the user is directed to the correct content after the app installation is complete.
Conclusion
Effectively handling deep links in Flutter is crucial for providing a seamless user experience and improving app engagement. By implementing Custom URL Schemes and Universal Links, developers can create robust deep linking mechanisms that direct users to specific content within the app from external sources. Following best practices such as implementing fallback URLs, handling errors gracefully, and thoroughly testing the implementation ensures that deep links function correctly and reliably across different devices and scenarios.