Deep linking is a crucial feature in modern mobile applications, allowing users to navigate directly to specific content within an app from external sources such as websites, emails, or social media links. In Flutter, implementing deep linking can significantly enhance user experience and engagement. This article provides a comprehensive guide to understanding and implementing deep linking in Flutter applications.
What is Deep Linking?
Deep linking is a technique that directs users to a specific location within an application rather than just opening the app’s homepage. This can be used for various purposes, such as:
- Navigating users directly to a product page from an advertisement.
- Directing users to a specific article from a social media post.
- Handling password reset links.
- Implementing referral programs.
Why Use Deep Linking in Flutter?
- Improved User Experience: Reduces friction by taking users directly to relevant content.
- Increased Engagement: Enhances user interaction and conversion rates.
- Seamless Navigation: Facilitates navigation from external sources into specific app sections.
Types of Deep Linking
There are primarily two types of deep links:
- Custom Scheme Deep Links: Uses a custom URL scheme (e.g.,
myapp://path/to/content
). - Universal Links (Android App Links/iOS Universal Links): Uses standard HTTP/HTTPS URLs that are associated with your website.
Custom Scheme Deep Links
Custom scheme deep links are easier to implement but less secure. They rely on a custom URL scheme registered by the app. If multiple apps register the same scheme, the system presents a disambiguation dialog to the user.
Universal Links
Universal Links provide a more secure and reliable deep linking solution. They use standard web URLs (http://
or https://
) and require verification through a file hosted on your website.
Implementing Deep Linking in Flutter: Custom Scheme
Let’s start by implementing custom scheme deep links in a Flutter application.
Step 1: Add Dependency
Add the uni_links
package to your pubspec.yaml
file:
dependencies:
flutter:
sdk: flutter
uni_links: ^0.5.1
Run flutter pub get
to install the dependency.
Step 2: Configure Native Platforms
Android Configuration
Open android/app/src/main/AndroidManifest.xml
and add the following intent filter inside your main activity (<activity>
tag):
<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>
Replace myapp
with your desired custom scheme. In this case, a URL like `myapp://open?param1=value1¶m2=value2` would trigger this intent filter.
iOS Configuration
Open ios/Runner/Info.plist
and add the following inside the <dict>
tag:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
</dict>
</array>
Replace myapp
with your desired custom scheme, and com.example.myapp
with your app’s bundle identifier.
Step 3: Implement Deep Link Handling in Flutter
In your Flutter application, listen for incoming deep links using the uni_links
package. Create a Flutter service or use your main app widget to handle incoming links.
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
class DeepLinkingService {
StreamSubscription? _sub;
void initialize(BuildContext context) {
_initDeepLinkHandling(context);
}
Future<void> _initDeepLinkHandling(BuildContext context) async {
// Platform messages may fail, so we use a try/catch PlatformException.
try {
final initialLink = await getInitialLink();
if (initialLink != null) {
_handleDeepLink(context, initialLink);
}
} catch (e) {
print('Error getting initial link: $e');
}
// Attach a listener to the stream
_sub = linkStream.listen((String? link) {
if (link != null) {
_handleDeepLink(context, link);
}
}, onError: (err) {
print('Error during deep link stream: $err');
});
}
void _handleDeepLink(BuildContext context, String link) {
// Parse the link and navigate accordingly
Uri uri = Uri.parse(link);
// Example: myapp://open?route=product&id=123
if (uri.scheme == 'myapp' && uri.host == 'open') {
String? route = uri.queryParameters['route'];
String? id = uri.queryParameters['id'];
if (route == 'product' && id != null) {
// Navigate to product details page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailsPage(productId: id),
),
);
} else {
// Handle other routes or display an error
print('Unknown route: $route');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Unknown deep link route')),
);
}
} else {
print('Invalid deep link: $link');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid deep link')),
);
}
}
void dispose() {
_sub?.cancel(); // Important to cancel the subscription
}
}
class ProductDetailsPage extends StatelessWidget {
final String productId;
ProductDetailsPage({required this.productId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Product Details'),
),
body: Center(
child: Text('Product ID: $productId'),
),
);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _deepLinkingService = DeepLinkingService();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_deepLinkingService.initialize(context);
});
}
@override
void dispose() {
_deepLinkingService.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Deep Linking Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Text('Welcome to the app!'),
),
),
);
}
}
Key points in this code:
- `DeepLinkingService` Class: A class responsible for initializing deep link handling, listening for incoming links, and disposing of resources.
- `_initDeepLinkHandling()` Method: Initializes deep linking by retrieving the initial link (if the app was opened via a deep link) and listening for subsequent links.
- `_handleDeepLink()` Method: Parses the deep link, extracts relevant information (e.g., route and ID), and navigates the user to the appropriate screen.
- `ProductDetailsPage` Widget: A placeholder widget representing a product details screen.
Implementing Deep Linking in Flutter: Universal Links
Implementing Universal Links involves configuring both your Flutter app and your website. This process verifies that you own both the app and the website, providing a more secure linking experience.
Step 1: Configure the Website
Create an apple-app-site-association
file and host it on your website under the .well-known
directory. This file associates your website with your app.
Example apple-app-site-association
file:
{
"applinks": {
"apps": [],
"details": [
{
"appID": "YOUR_TEAM_ID.com.example.myapp",
"paths": [
"/product/*",
"/article/*"
]
}
]
}
}
Replace YOUR_TEAM_ID.com.example.myapp
with your app’s Team ID and Bundle Identifier. The paths
array specifies which URL paths should be handled by your app.
Ensure the file is served with the MIME type `application/json` and is accessible over HTTPS.
Android equivalent assetlinks.json file (place in /.well-known directory):
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints":
["YOUR_SHA256_FINGERPRINT"]
}
}]
Replace `com.example.myapp` with your app’s package name and `YOUR_SHA256_FINGERPRINT` with the SHA256 fingerprint of your signing certificate. The SHA256 fingerprint can be obtained using keytool.
Step 2: Configure the Flutter App
iOS Configuration
Open your Xcode project, navigate to your target’s settings, and enable the “Associated Domains” capability. Add an entry for your website’s domain with the applinks:
prefix:
applinks:yourdomain.com
Replace yourdomain.com
with your actual domain.
Android Configuration
Add the following to your `AndroidManifest.xml` file, within the `
<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="http" android:host="yourdomain.com" />
<data android:scheme="https" android:host="yourdomain.com" />
</intent-filter>
Replace `yourdomain.com` with your actual domain.
Make sure `android:autoVerify=”true”` attribute is present to enable automatic verification of your App Links.
Step 3: Handle the Universal Links in Flutter
Use the same uni_links
package to listen for and handle Universal Links in your Flutter app.
import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
class DeepLinkingService {
StreamSubscription? _sub;
void initialize(BuildContext context) {
_initDeepLinkHandling(context);
}
Future<void> _initDeepLinkHandling(BuildContext context) async {
try {
final initialLink = await getInitialLink();
if (initialLink != null) {
_handleDeepLink(context, initialLink);
}
} catch (e) {
print('Error getting initial link: $e');
}
_sub = linkStream.listen((String? link) {
if (link != null) {
_handleDeepLink(context, link);
}
}, onError: (err) {
print('Error during deep link stream: $err');
});
}
void _handleDeepLink(BuildContext context, String link) {
Uri uri = Uri.parse(link);
// Example: https://yourdomain.com/product?id=123
if (uri.scheme == 'https' && uri.host == 'yourdomain.com') {
String? path = uri.path;
if (path.startsWith('/product')) {
String? productId = uri.queryParameters['id'];
if (productId != null) {
// Navigate to product details page
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailsPage(productId: productId),
),
);
} else {
// Handle missing product ID
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Product ID is missing')),
);
}
} else {
// Handle other routes or display an error
print('Unknown route: $path');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Unknown deep link route')),
);
}
} else {
print('Invalid deep link: $link');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Invalid deep link')),
);
}
}
void dispose() {
_sub?.cancel();
}
}
class ProductDetailsPage extends StatelessWidget {
final String productId;
ProductDetailsPage({required this.productId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Product Details'),
),
body: Center(
child: Text('Product ID: $productId'),
),
);
}
}
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _deepLinkingService = DeepLinkingService();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_deepLinkingService.initialize(context);
});
}
@override
void dispose() {
_deepLinkingService.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Deep Linking Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: Scaffold(
appBar: AppBar(
title: Text('Home'),
),
body: Center(
child: Text('Welcome to the app!'),
),
),
);
}
}
Testing Universal Links
To test Universal Links, you can simulate a click on a link in a note, message, or website on a physical device. Make sure that the app is not running when you test the link. Also, the `apple-app-site-association` or `assetlinks.json` file should be correctly set up on your server before you test universal links.
Debugging Deep Linking
Debugging deep linking issues can be challenging. Here are some tips:
- Check Native Configurations: Ensure your
AndroidManifest.xml
andInfo.plist
are correctly configured. - Verify Website Setup: Make sure your
apple-app-site-association
and `assetlinks.json` file is correctly placed and accessible on your website. - Use Logging: Add detailed logging to track the deep linking process in your Flutter app.
- Test on Real Devices: Deep linking behavior can vary on emulators and simulators, so test on real devices.
- Clear App Data: Sometimes clearing app data or reinstalling the app can resolve deep linking issues.
Conclusion
Deep linking is a powerful tool for improving user experience and engagement in Flutter applications. By implementing custom scheme deep links or Universal Links, you can seamlessly navigate users to specific content within your app from external sources. Understanding the differences between custom schemes and Universal Links, correctly configuring your app and website, and implementing robust error handling are essential for successful deep linking implementation. By following this guide, you can effectively integrate deep linking into your Flutter projects.