Sep 25, 2024

How to Implement Branch Deep Linking in Flutter

Learn how to implement deep and deferred linking using Branch.io in Flutter, allowing seamless user navigation and enhanced app engagement. Follow this step-by-step guide to set up deep links for Android, iOS, and social media previews.

Author

Akanksha V DeshpandeTechnical Project Manager
How to Implement Branch Deep Linking in Flutter

Deep linking is a powerful technique that takes users exactly where they need to go within an app, skipping the generic home screen and landing on the desired content or specific section. By enabling direct navigation, deep linking streamlines the user journey, enhances engagement and delivers a smoother, faster app experience.


Everyday Deep Linking in Action

Imagine these scenarios:

  • You receive an Instagram reel shared via WhatsApp. A simple tap doesn’t just open Instagram – it takes you straight to the reel, making the whole process seamless.
  • A friend invites you to a fitness app using a referral link. Click the link, and you're not only directed to the app’s sign-up form but the referral code is automatically passed along, ensuring you never miss out on the benefits.
  • Someone sends you a song via YouTube. When you tap the link, the YouTube app launches and the song starts playing instantly
  • New users clicking on onboarding emails can be directed to specific parts of the app, such as tutorials or account setup screens.
  • Event Invitations, Marketing Campaigns, Customer Support, etc.

Deep linking plays a crucial role in driving user retention, improving user experience, and enhancing app marketing strategies.


How Deep Linking Works: A Behind-the-Scenes Look

Here’s the magic behind the curtain:

  • App Detection: The system uses a domain URL to check if the app is installed on the device (iOS/Android).
  • Link Handling: Once the app is found, a listener function kicks in, waiting for incoming links.
  • Navigation: The app handles the link, determining where the user should land, based on the data or route embedded in the link.

Now, what if the user does not have the application installed?

That’s where Deferred Links come into play. Think of them as deep links with a twist – they don’t just stop when the app is not installed; they keep the journey going!

Deferred Links have the unique ability to seamlessly navigate users to the appropriate app store (whether it’s the App Store or Play Store), or to a fallback URL if configured. When a user clicks a deep link without having the app installed, instead of getting lost, the deferred link takes charge. It redirects the user to the correct destination first, ensuring they install the app, and then, post-install, it automatically navigates them to the content embedded in the link.

Branch.io offers the best of both worlds: not only deep links but also the magic of deferred links, making sure your users never miss a beat, even if they’re new to your app.


Setup Branch Account

You can create an account by signing up at Branch.io. After creating an account, your dashboard will look like the one below.

https://lh7-rt.googleusercontent.com/docsz/AD_4nXcWaeiNwDX_7qu_2WMlV_ulhklAnbH8rrhDJFjHCqz6y40MgxlpQynMP3EQHAmyIysclfTvv8XMnVA37pBiGLnv4e3FFZwn2d9kAHL2MXpBLN1C_rnvtAe-1U4_aifznEpfvthmyl3DLENvZBfxRMwSZ0We?key=oXUjUcRztgxOBV21Q_ENkw

While Branch offers free mobile deep linking for your first 10k conversions/links, you'll still need to enter your credit card details to access the feature. Once your billing is set up, you're ready to go!

Next, head over to the Configuration tab in the dashboard’s menu to start setting up your Android and iOS platforms.


Branch Android Configuration

  1. Android URI scheme: It has to be in this format “<Your App Name>://”. Anyway, the field in the dashboard guides you with the format.
  2. Check on the “I have an Android app” option. If you are already live then search your app on PlayStore or select Custom URL - your app’s Play Store listing (e.g., https://play.google.com/store/apps/details?id=com.example.yourapp). This is to make sure that if the app is not installed then the fallback re-direction is set.
  3. Check on the “Enable app links” option. You are supposed to provide SHA-256. Input the SHA-256 fingerprint of your app's certificate (used to sign your APK). This ensures Branch can verify your app’s authenticity.

To get this fingerprint, run the following command in your terminal:

keytool -list -v -keystore [your_keystore_path] -alias [your_key_alias] -storepass [your_keystore_password] -keypass [your_key_password]


Branch iOS Configuration

  1. iOS URI scheme: Same as Android configuration. “<Your App Name>://” .
  2. Check on the “I have an iOS app” option. If you are already live then search your app on the AppStore or select Custom URL. Input your app’s Apple App Store ID. You can find this in your app’s App Store listing URL. - e.g. https://apps.apple.com/app/id123456789, then 123456789 is your App Store ID.
  3. Enable Universal Links. Enter your app’s bundle identifier (e.g., com.example.yourapp). This must exactly match the bundle ID specified in your Xcode project under General > Identity. Enter your Apple Developer Team ID in place of the Apple App prefix.

You can take it a step further by configuring how your link looks when shared on social media.

Add a personal touch with a custom image, title, and description to create eye-catching previews.

How cool is that? Your shared links won’t just function well—they’ll look lively and engaging too!

https://lh7-rt.googleusercontent.com/docsz/AD_4nXeBDflv_4upgs3SSpVxhH6lcxjdzpJ-pbtA-qNCKZB__LBC4Kqyw251J7eG38lqcP23Zgq1HYTTjRjadnEA_d83wduZsijFpjMm_IxN1P91c2gnFBueBp0I6RQpledenRJguD1mILq1I63L3vdbATkCDqpd?key=oXUjUcRztgxOBV21Q_ENkw

Branch.io provides a default domain URL (something like c8prt.app.link), but why stop there?

You can easily create your own custom domain to make your links more personalized and on-brand.

Already have a domain? Even better—you can use it seamlessly with Branch!

https://lh7-rt.googleusercontent.com/docsz/AD_4nXdXxPzB1MLFoWxsBVbJyweVdEko9PuhHNYMElqlXpvW8JAwiyhqFiZX5HzZQLDyuVio93MbNo8fyNyDvaaE-hgsMTMkK-1lXA-3W91pLkjazcsro7Mw0R-xBVrAy92kmQbBD8RvxmbJYRShd_lyorubZ3Gu?key=oXUjUcRztgxOBV21Q_ENkw

Be sure to take note of your Branch Key, Branch Secret Key, App Name, and App ID in the Account Settings.

https://lh7-rt.googleusercontent.com/docsz/AD_4nXd3mP5Nvw5-VZURRzHJ0k2QmZ-gB7zOrfQBxiTsQ8yEeZ7HdBtQvCTrZt-gYqRZW_2MTEK_YxrwHks8UCjF7i5PgW2NUPX1AsvUdcvOanXGiBhRGH7GpQqicuHDlVMfn4889B_4p_S4N3LV3t8iIkncgCM?key=oXUjUcRztgxOBV21Q_ENkw


Integrating Branch into Flutter: The Flutter Branch SDK

Start by integrating the Flutter Branch SDK.

You can easily find the plugin here flutter_branch_sdk

$ flutter pub add flutter_branch_sdk


Android Integration

In your main AndroidManifest.xml file, make the launchMode to SingleTask to ensure only one instance of the activity exists at a time. If an instance already exists, it will bring it to the foreground instead of creating a new instance.

<activity
            android:name=".MainActivity"
            android:exported="true"  
            android:launchMode="singleTask"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
 ...
</activity>
Inside the activity tag, add the following intents:
<activity
...
	 	 		   <intent-filter>
                <data android:scheme="branch" android:host="open" />
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
            </intent-filter>
            
             <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="branchiolink.app.link" />
                <data android:scheme="https" android:host="branchiolink-alternate.app.link" />
            </intent-filter>
...
</activity>

Add the following in the application tag
	<application
	...
	<meta-data android:name="io.branch.sdk.BranchKey" android:value="key_live_nBbMPdDcPTarHHbVnHZ50ddiDwoL6YJq" />
	<meta-data android:name="io.branch.sdk.BranchKey.test" android:value="key_test_krmULdukNJcsRJkVdU7Z1okjDrlQ2ZNI" />
	<meta-data android:name="io.branch.sdk.TestMode" android:value="fasle" />
	...
	</application>

Note: If you are developing in Test mode then io.branch.sdk.TestMode should be set to true.


iOS Integration

The first step is to ensure that your Bundle Identifier in Xcode matches the Bundle ID you set during the Branch configuration.

In Xcode, navigate to the Capabilities section and enable Associated Domains. Simply click the "+" button to add the domain links generated by Branch.

Coming to Info.plist to add Branch key, domain links and URI scheme.
For domain link :

	<key>branch_universal_link_domains</key>
 	<array>
 		<string>branchiolink.app.link</string>
		<string>branchiolink-alternate.app.link</string>
 	</array>

For URL scheme :

	 <key>CFBundleURLSchemes</key>
	 <array>
	 		<string>branch</string>
 	 </array>

For Branch Key:

 <key>branch_key </key>
 <string>key_live_izaO2UairIhXj0ZrTZTJmmmguwp7XD5d</string>

Now comes the exciting part—bringing your Branch links to life! Let’s walk through how to seamlessly generate and handle Branch links within your Flutter app.

Start by importing the Flutter Branch SDK into your main.dart file, then initialize it to get everything up and running.

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

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await FlutterBranchSdk.init(enableLogging: true, disableTracking: false);
  runApp(const MyApp());
}

To ensure everything is set up correctly, you can verify the SDK integration by running the following command:

FlutterBranchSdk.validateSDKIntegration();

After validating, you can comment this line.
Now, let’s set up listener to capture deep link clicks and retrieve the embedded data that comes along with them:

 void branchLinkListener(Map<String, dynamic> data) {
    streamSubscription = FlutterBranchSdk.listSession().listen((data) {
      if (data.containsKey("+clicked_branch_link") && data["+clicked_branch_link"] == true) {
        print('$data');
        //route to respective screen by reading data
      }
    }, onError: (error) {
      print('listSession error: ${error.toString()}');
    });
  }

The process of generating a link follows a straightforward formula!
To make things clearer, let’s dive into a practical example and walk through the integration step by step. It’s always easier to grasp when you see it in action!


To Generate a Branch Link for Sharing a Post / Reel / Blog

First, we encapsulate the content and metadata of what we want to share using BranchUniversalObject. This object acts as the core of your content, carrying details like the title, description, and other relevant information.
To define how the link behaves and If you are keen on tracking the performance of your links—be it through analytics or specific campaigns—fine-tune the link’s behaviour using BranchLinkProperties - Here, you can specify important elements like tags, channels, campaigns, and other tracking parameters.

void generateBranchLinkForPost(String postId, String postTitle, String postDescription) async {
  // Step 1: Create BranchUniversalObject with the post details
  BranchUniversalObject branchLinkData = BranchUniversalObject(
    canonicalIdentifier: 'post/$postId',
    title: postTitle,
    contentDescription: postDescription,
    imageUrl:'post_image_url',
    keywords: ['Post', 'SEO', 'Flutter'],
    contentMetadata: BranchContentMetaData()
          ..addCustomMetadata('post_owner_name', 'akanksha')
          ..addCustomMetadata('post_likes', 10)
  );
}

It is required to provide Link properties before we can generate the branch link.
In our use case, for sharing of post: 

  BranchLinkProperties branchLinkProperties = BranchLinkProperties(
    channel: 'app',        // The channel where the link will be shared, e.g., app, email, social media
    feature: 'share',      // The feature of the app (e.g., sharing) where the link is created
    campaign: 'post_share',// Campaign for tracking purposes
    stage: 'new_user',     // You can use this to track the lifecycle stage of the user (optional)
    tags: ['post', 'share'] // Tags for analytics
  );

Finally, generate the link

BranchResponse response = await FlutterBranchSdk.getShortUrl(buo: branchLinkData, linkProperties: branchLinkProperties);
if (response.success) {
    String branchLink = response.result;
    print('Branch link generated: $branchLink');
    // You can now share this link (e.g., via social media, SMS, etc.)
  } else {
    print('Error generating Branch link: ${response.errorMessage}');
  }

In the use case above, handling routing within the listener is essential. Since the postId value is extracted from the deep link, you can use it to fetch the full post details from your backend. To navigate to the correct screen, simply adjust your navigation logic.
It will look something like this:

 void branchLinkListener(Map<String, dynamic> data) {
    streamSubscription = FlutterBranchSdk.listSession().listen((data) {
      if (data.containsKey("+clicked_branch_link") && data["+clicked_branch_link"] == true) {
        print('$data');
        if (data.containsKey("post_id")) {
             String postId = data["post_id"];
		         // Navigate to the post screen
		         navigateToPostScreen(postId);        
      }
    }, onError: (error) {
      print('listSession error: ${error.toString()}');
    });
  }
  
 void navigateToPostScreen(String postId) {
		 Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => PostScreen(postId: postId),
      ),
    );
 }

The code above seamlessly manages deep links whether the app is running, in the background, or opened from a cold start (completely closed). When the app launches via a Branch link, even from a cold start, the deep link data is passed through the initSession() method, ensuring smooth navigation.
Branch links are more than just links—they’re intelligent!
Branch’s smart links work effortlessly across platforms. If the app is not installed, these links automatically detect the user’s platform and redirect them to the appropriate destination—whether it's Google Play or the App Store. For apps not yet live, you can easily set up a fallback URL to guide users. If your app is live, just configure the Branch dashboard to point directly to your app’s store listing. It’s that simple—whether for deep linking or deferred linking, Branch handles it all with ease!


Voilà! We’ve Reached the Finish Line

As I wrapped up my dive into deep and deferred linking with Branch, I discovered that Branch offers so much more than just linking. The Flutter SDK opens the door to a wealth of features—tracking user actions and events, working with first and last parameters, managing views, using share sheets, and beyond.

Excited to explore these possibilities even further—stay tuned!

Subscribe to Our Newsletter

RELATED ARTICLES

More from the engineering frontline.

Dive deep into our research and insights on design, development, and the impact of various trends to businesses.
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
The Bug That Doesn't Show Up in Code Review: Why Your Flutter Web App Reloads on Safari
A real-world look at how oversized images can trigger Safari reloads and iOS crashes in Flutter apps and how smarter image decoding prevents them.
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
From Prompting to Process: What Changed When Flutter Shipped Agent Skills
This blog explores how Flutter Agent Skills improve AI-assisted development by combining official framework workflows with project-specific guidance for more consistent development.
Why Everything Your AI Builds Looks the Same
Why Everything Your AI Builds Looks the Same
This blog explores why AI-generated interfaces often look alike and explains how design systems, product context, and reusable engineering practices help teams build distinctive, scalable
How We Built the Missing Bridge from Code to Figma
Technology

Jul 10, 2026

How We Built the Missing Bridge from Code to Figma
This blog explores how AI-generated React apps get turned into fully editable, designer-ready Figma files by reading React Fiber instead of the DOM.
Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
Technology

Jun 27, 2026

Building a Resilient Hybrid-Cloud Network with WireGuard HA, Route-Based Failover, and Deep Observability
A practical breakdown of building resilient AWS-to-on-premises connectivity with WireGuard HA, active-standby failover, and deep packet-forwarding observability.
We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
Technology

Jun 19, 2026

We Built a 114-Second AWS-to-Azure Failover. Here’s What We Learned
A practical guide to building a 114-second multi-cloud disaster recovery failover between AWS and Azure — what we built, what broke, and what we learned.
Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
Technology

Jun 12, 2026

Cloud-Native and Cloud-Agnostic Are Not Ideologies; They Are Business-Stage Decisions
This blog explains how organizations can balance speed, scalability, and operational flexibility as they grow from startup to enterprise scale.

The Right Conversation Can Save You Six Months.

Whether you’re navigating AI adoption, modernizing legacy systems, or scaling a product - we start by listening. No pitch deck. No template. A real conversation.