Mastering Deep Linking: A Comprehensive Guide for Flutter Developers

Sadiq
9 min readMay 7, 2024

--

Flutter, Google’s innovative UI toolkit, empowers developers to create stunning, high-performance apps for mobile, web, and desktop platforms — all from a single codebase. With its lightning-fast hot reload feature, Flutter accelerates development cycles, fostering creativity and efficiency. Deep linking is integral to Flutter, facilitating smooth in-app navigation and direct access to content from external sources like websites or other apps. By guiding users to specific screens or pages within the app, deep links enhance engagement and user experience. Implementing deep linking ensures seamless navigation across different sections of the app, ultimately enhancing usability and user retention.

What is DeepLinking?

Deep linking is a powerful technique that enables users to navigate directly to specific content or features within a mobile app, bypassing the app’s home screen. It facilitates seamless integration between different platforms and enhances user experience by providing instant access to relevant content. Deep linking plays a vital role in marketing strategies, user engagement, and app retention efforts.

Usage of Deep Linking in Flutter-app development?

Deep linking plays a crucial role in Flutter by enabling seamless navigation within apps. It allows users to access specific content or features directly from outside sources, such as a website or another app. Deep links can guide users to a particular screen or page within the Flutter app, enhancing user experience and engagement. By implementing deep linking, developers can ensure that users can effortlessly navigate through different sections of the app, improving usability and retention.

Deep Linking — A bigger picture

Deep Linking in Android

Configuration steps are given below.

  1. Open the AndroidManifest.xml file.
  2. Define intent filters for each deep link you want to support within the <activity> tags. For example:
Intent Filter — Manifest.xml
<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />

<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="example.com"
/>
<data
android:scheme="http"
android:host="example.com"
/>
</intent-filter>

3. Publish assetlinks.json file on you server. It’s file path on server must be like this one : https://example.com/.well-known/assetlinks.json.

To retrieve the SHA-256 key, navigate to the Android folder within your project directory and execute the following command:

./gradlew signingReport
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.test",
"sha256_cert_fingerprints":
["***************************************************************************"]
}
}]

Incase of delpoying the App on Google Play Store.
You have to retrieve the SHA-256 fingerprint from the Google Play Console, access the Google Play Console, select your app, navigate to Setup > App Integrity > App signing, and view the “App signing certificate” section, where you’ll find the SHA-256 fingerprint along with other certificate details. Copy the SHA-256 fingerprint and paste it under assetlinks.json file and deploy it on server.

4. Starting from Android 12 and above, it’s essential to publish your assetlinks.json file on a server with a secure HTTPS connection. Failure to do so will require users to manually enable deep linking from the app settings. This change ensures enhanced security and prevents potential security risks associated with deep linking.

For development build, Please go to App info -> Open by default -> Add link -> check off all the available links.

5. Test your deep link by using the command provided.

adb shell am start -W -a android.intent.action.VIEW -d "your deep link url"

Diagnose Deep Link in Android(Incase if not working)

1. If your deep link still isn’t working, simply verify if the assetlinks.json is present and set up correctly using this site: Deep Link Validator/Tester at https://developers.google.com/digital-asset-links/tools/generator .

Deep Linking in iOS

Configuration steps(using xcode) are given below.

  1. Open your ios/Runner.xcworkspace in Xcode.
  2. Inside your Xcode, navigate to the “Runner” target, then click on “Signing & Capabilities.” Within “Signing & Capabilities” add the Associated Domains capability by clicking on the + Capability button.

3. After adding Associated Domains in your Capability you need add your domain in the form like your domain look like applinks:example.com .

4. Also, make sure to enable Associated Domains from your app identifier section inside your Apple Developer account.

5. To enable deep linking in iOS, you’ll need to create an apple-app-site-association file and host it on your server. The route to this file should resemble https://example.com/.well-known/apple-app-site-association. Apple-app-site-association file content are given below.

{
"applinks": {
"apps": [],
"details": [{
"appID": "teamId.com.example.test",
"paths": ["/*"]
}]
}
}

Within the file, in the appID section, replace teamId with your Team ID, which you can find in your Apple Developer account under “Membership details.” Then, substitute com.example.test with your Bundle Identifier, accessible in Xcode under the General tab within the Runner section.

Requirement for AASA file(apple-app-site-association)
*
The host must be published on an HTTPS domain.
*
The AASA file should be located inside the .well-known folder or at the root level.
*
The file size should not exceed 128 KB
*
The file should not have any suffix such as .json, .php, .aspx, .html, etc.
*
The Content-Type header should be set to application/json.

6. Test your deep link in iOS application by running the following command in terminal.

xcrun simctl openurl booted https://example.com

If you have routes or query parameters

xcrun simctl openurl booted "http://example.com/verify-link?q=asd34fdfsd"

Diagnose Deep Link in iOS(Incase if not working)

  1. You can validate Apple-App-Site-Association weather it was perfectly install or upload on your server or not from this AASA Validator site https://branch.io/resources/aasa-validator/
  2. Open your physical device(iPhone) setting and if your developer mode is enable navigate to Developer. Inside Developer there is an item name Diagnostics press on Diagnostics. There is an input field with URL add your domain at a place of https://example.com in input field and check weather it was working or not.
    Screenshots are attached for the guidance.
Verify your domain from iPhone

3. Press the volume up and down buttons along with the power button simultaneously until you feel a haptic feedback (it will come within 2–3 seconds), indicating that your phone has been locked. After 10–15 minutes, navigate to iPhone Settings > Privacy & Security. Scroll down and tap on Analytics or Analytics & Improvements. Inside Analytics & Improvements, click on Analytics Data. You’ll find multiple files that have been operating and existing on your phone. Among these files, locate the sys-diagnostic file, which will have a timestamp like (sys-diagnostic_2024.04.30) indicating it was newly created.
Screenshots will be attached to assist you.

Getting sys-diagnostic file

Share sys-diagnostic_2024.04.30 file with your Mac or another system and unzip the folder. Once extracted, you’ll find a file named swcutil_show.txt. Open this file and search for your Bundle Identifier, such as com.example.test. If there are any issues with configuring your deep link, you’ll see an error block. Otherwise, you’ll see “User Approval Verified” within your application’s Bundle Identifier section.

After sharing the file to the system, follow these steps to diagnose the issue.
From given screenshot you will get swcutil_show.txt file

swcutil_show.txt

Once you open your swcutil_show.txt file and search for your Bundle Identifier, If your deep link is verified, you’ll see approved as indicated in the screenshot below.

success

If you’re link is not verified then you will be getting an error like below mentioned in screenshot.

Error

Lets dive into code

We are using uni_links package inorder to handle deep linking in flutter. Please install the package by running the following command

flutter pub add uni_links

Or directly add in pubspec.yaml file:

dependencies:
uni_links: ^x.x.x

Create services folder inside the lib. Create file inside services name context_utility.dart and paste the following code inside that file.

import 'package:flutter/material.dart';

class ContextUtility {
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>(debugLabel: 'ContextUtilityNavigatorKey');
static GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;

static bool get hasNavigator => navigatorKey.currentState != null;
static NavigatorState? get navigator => navigatorKey.currentState;

static bool get hasContext => navigator?.overlay?.context != null;
static BuildContext? get context => navigator?.overlay?.context;
}

Above context_utility file provides access to the navigator and context of the current Flutter application through static methods and variables, making it easier to manage navigation and access context throughout the app.

After creating context_utility file need to create in more file inorder to handle navigation for deep linking inside the application.
Create file inside services folder name uniLink_services.dart. And just add the following code in that file.

class UniLinksService {
static String _promoId = '';
static String get promoId => _promoId;
static bool get hasPromoId => _promoId.isNotEmpty;

static void reset() => _promoId = '';

static Future<void> init({bool checkActualVersion = false}) async {
// This is used for cases when: APP is not running and the user clicks on a link.
try {
final Uri? uri = await getInitialUri();
_uniLinkHandler(uri: uri);
} on PlatformException {
if (kDebugMode) print("(PlatformException) Failed to receive initial uri.");
} on FormatException catch (error) {
if (kDebugMode) print("(FormatException) Malformed Initial URI received. Error: $error");
}

// This is used for cases when: APP is already running and the user clicks on a link.
uriLinkStream.listen((Uri? uri) async {
_uniLinkHandler(uri: uri);
}, onError: (error) {
if (kDebugMode) print('UniLinks onUriLink error: $error');
});
}

static Future<void> _uniLinkHandler({required Uri? uri}) async {
if (uri == null || uri.queryParameters.isEmpty) return;
String? params = uri.queryParameters['tok'];
await storeDeepLinkToken(params!);
if (uri.path == '/verify-email') {
ContextUtility.navigator?.pushReplacement(
MaterialPageRoute(builder: (_) => LoginScreen(verificationToken: params)),
);
}else if(uri.path == '/reset_password'){
ContextUtility.navigator?.push(
MaterialPageRoute(builder: (_) => NewPasswordScreen(verificationToken: params,)),
);
}
}
}

In above file uniLink_services.dart handles deep linking within the Flutter application. It initializes and listens for URI changes, and when a deep link is detected, it handles the link based on specific conditions. It also utilize the aboveContextUtility class to manage navigation within the app, such as pushing new screens onto the navigation stack.

In main.dart, the UniLinksService.init() function is called to initialize deep linking functionality. The MyApp widget sets up the application and defines the routes for default navigation using the GetMaterialApp widget. The routes are defined to map specific URLs to corresponding screens within the app.

void main() async {
await UniLinksService.init();
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {

@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: (context, child) => GetMaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: ContextUtility.navigatorKey,
title: 'Test Application',
theme: getAppTheme(),
darkTheme: getDarkTheme(),
themeMode: ThemeMode.system,
routes: {
'/':(_) => const SignUpScreen(),
'/verify-email': (_) => LoginScreen(),
'/reset_password' : (_) => const NewPasswordScreen()
},
));
}
}

Another way if the app isn’t functioning properly or isn’t navigating to specific screens as expected from above code, You can simplify the code by updating just your main.dart file. Just add the onGenerateRoute parameter to your GetMaterialApp widget and set it to the generateRoute function for managing the navigation. This way, you won't need to create separate files like context_utility or uniLink_services.

void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});

@override
State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
String data = 'testUser';
return ScreenUtilInit(
designSize: const Size(375, 812),
builder: (context, child) => GetMaterialApp(
debugShowCheckedModeBanner: false,
title: 'Test Application',
themeMode: ThemeMode.system,
initialRoute: '/',
onGenerateRoute: generateRoute,
));
}
static Route<dynamic> generateRoute(RouteSettings settings) {
final uri = Uri.parse(settings.name!); // Parse the route to extract path and query parameters

switch (uri.path) {
case '/':
return MaterialPageRoute(builder: (_) => SignUpScreen());
case '/verify-email':
// Extract token if available
final token = uri.queryParameters['tok'];
// Navigate to LoginScreen
return MaterialPageRoute(builder: (_) => LoginScreen());
case '/reset_password':
// Extract token if available
final token = uri.queryParameters['tok'];
// Navigate to NewPasswordScreen with token
return MaterialPageRoute(builder: (_) => NewPasswordScreen());
default:
// If the route is not matched, navigate to Signup screen
return MaterialPageRoute(builder: (_) => SignUpScreen());
}
}
}

The output of the experiment showed promising results

Thank you for reading until the end. Your support means a lot!

--

--