Implementing Deep Linking for Direct Navigation in Flutter

Deep linking in Flutter allows users to navigate directly to specific sections or pages within an app from external sources, such as web pages, emails, or other applications. This powerful feature enhances the user experience by providing seamless navigation and direct access to relevant content. Implementing deep linking in Flutter can be achieved through various methods, each with its own advantages and considerations.

What is Deep Linking?

Deep linking is a technique that uses a uniform resource identifier (URI) to link to a specific location within a mobile application rather than simply launching the app. This allows the application to respond to the URI by navigating the user directly to the specified content or action. There are primarily two types of deep linking: standard deep linking and deferred deep linking. Standard deep linking works when the app is already installed on the device, while deferred deep linking handles the case where the app needs to be installed first.

Why Use Deep Linking?

  • Enhanced User Experience: Provides direct access to specific content, reducing friction.
  • Improved Engagement: Drives users from external sources directly to relevant app features.
  • Better Conversion Rates: Streamlines the user journey for targeted actions.

How to Implement Deep Linking in Flutter

Implementing deep linking in Flutter involves several steps, including configuring the app manifest for URI schemes and handling the incoming links within the app.

Method 1: Using the url_launcher Package

The url_launcher package is a common and straightforward way to handle deep links in Flutter. It allows your app to open URLs, and by parsing these URLs, you can navigate within your application.

Step 1: Add Dependency

Add the url_launcher package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  url_launcher: ^6.1.5

Then, run flutter pub get to install the package.

Step 2: Configure the App Manifest (Android)

In your AndroidManifest.xml file (located in android/app/src/main/), add an intent-filter to the <activity> tag. This filter will capture specific URI schemes that your app can handle.

<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"/> <!-- Replace with your scheme -->
        <data android:host="open"/>   <!-- Replace with your host -->
    </intent-filter>
</activity>

Replace "myapp" with your desired URI scheme and "open" with your host.

Step 3: Configure the App (iOS)

For iOS, you need to configure the Info.plist file (located in ios/Runner/) to specify the URL schemes that your app can handle.

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string> <!-- Replace with your scheme -->
        </array>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string> <!-- Replace with your app's bundle identifier -->
    </dict>
</array>

Replace "myapp" with your desired URI scheme and "com.example.myapp" with your app’s bundle identifier.

Step 4: Handle Incoming Links in Flutter

In your Flutter app, use the url_launcher package to listen for incoming links and navigate accordingly. Here’s an example using a StatefulWidget:

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
import 'dart:async';

class DeepLinkingHandler extends StatefulWidget {
  @override
  _DeepLinkingHandlerState createState() => _DeepLinkingHandlerState();
}

class _DeepLinkingHandlerState extends State<DeepLinkingHandler> {
  String? _incomingLink;
  StreamSubscription? _sub;

  @override
  void initState() {
    super.initState();
    _initDeepLinkHandling();
  }

  Future<void> _initDeepLinkHandling() async {
    // Check if app was started with a link
    final initialLink = await getInitialUri();
    if (initialLink != null) {
      setState(() {
        _incomingLink = initialLink.toString();
      });
      _handleDeepLink(initialLink);
    }

    // Subscribe to receive updates when app is already running
    _sub = uriLinkStream.listen((Uri? uri) {
      if (uri != null) {
        setState(() {
          _incomingLink = uri.toString();
        });
        _handleDeepLink(uri);
      }
    }, onError: (err) {
      print('Error receiving URI: $err');
    });
  }

  void _handleDeepLink(Uri link) {
    // Example: myapp://open?route=/details&id=123
    if (link.host == 'open') {
      final route = link.queryParameters['route'];
      final id = link.queryParameters['id'];

      if (route == '/details' && id != null) {
        Navigator.of(context).push(MaterialPageRoute(
          builder: (context) => DetailsPage(id: id),
        ));
      }
    }
  }

  @override
  void dispose() {
    super.dispose();
    _sub?.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Deep Linking Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Incoming Link:',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            Text(_incomingLink ?? 'No link received'),
          ],
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  final String id;
  DetailsPage({required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details Page'),
      ),
      body: Center(
        child: Text('Details ID: $id'),
      ),
    );
  }
}

This code does the following:

  • It initializes the url_launcher plugin and listens for incoming links using getInitialUri() and uriLinkStream.
  • It checks if the app was started with a deep link by calling getInitialUri() in the initState() method.
  • It subscribes to the uriLinkStream stream, which emits incoming URIs while the app is running.
  • The _handleDeepLink() method parses the link and navigates to the appropriate screen using Navigator.push().

Method 2: Using the uni_links Package

The uni_links package provides a more robust solution for handling deep links, including deferred deep linking (handling links after the app is installed).

Step 1: Add Dependency

Add the uni_links package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.5.1

Then, run flutter pub get to install the package.

Step 2: Configure the App Manifest (Android)

As with the url_launcher method, add an intent-filter to your AndroidManifest.xml file:

<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"/> <!-- Replace with your scheme -->
        <data android:host="open"/>   <!-- Replace with your host -->
    </intent-filter>
</activity>

Replace "myapp" with your desired URI scheme and "open" with your host.

Step 3: Configure the App (iOS)

For iOS, configure the Info.plist file as well:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string> <!-- Replace with your scheme -->
        </array>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string> <!-- Replace with your app's bundle identifier -->
    </dict>
</array>

Replace "myapp" with your desired URI scheme and "com.example.myapp" with your app’s bundle identifier.

Step 4: Handle Incoming Links in Flutter

Here’s an example of using uni_links to listen for incoming links:

import 'package:flutter/material.dart';
import 'package:uni_links/uni_links.dart';
import 'dart:async';
import 'package:flutter/services.dart';

class DeepLinkingHandler extends StatefulWidget {
  @override
  _DeepLinkingHandlerState createState() => _DeepLinkingHandlerState();
}

class _DeepLinkingHandlerState extends State<DeepLinkingHandler> {
  String? _incomingLink;
  StreamSubscription? _sub;

  @override
  void initState() {
    super.initState();
    _initDeepLinkHandling();
  }

  Future<void> _initDeepLinkHandling() async {
    try {
      // Check if app was started with a link
      final initialLink = await getInitialLink();
      if (initialLink != null) {
        setState(() {
          _incomingLink = initialLink;
        });
        _handleDeepLink(initialLink);
      }

      // Subscribe to receive updates when app is already running
      _sub = linkStream.listen((String? link) {
        if (link != null) {
          setState(() {
            _incomingLink = link;
          });
          _handleDeepLink(link);
        }
      }, onError: (err) {
        print('Error receiving URI: $err');
      });
    } on PlatformException {
      print('Failed to get initial link.');
    } on FormatException {
      print('Failed to parse initial link as Uri.');
    }
  }

  void _handleDeepLink(String link) {
    final uri = Uri.parse(link);
    // Example: myapp://open?route=/details&id=123
    if (uri.host == 'open') {
      final route = uri.queryParameters['route'];
      final id = uri.queryParameters['id'];

      if (route == '/details' && id != null) {
        Navigator.of(context).push(MaterialPageRoute(
          builder: (context) => DetailsPage(id: id),
        ));
      }
    }
  }

  @override
  void dispose() {
    super.dispose();
    _sub?.cancel();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Deep Linking Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'Incoming Link:',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            Text(_incomingLink ?? 'No link received'),
          ],
        ),
      ),
    );
  }
}

class DetailsPage extends StatelessWidget {
  final String id;
  DetailsPage({required this.id});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Details Page'),
      ),
      body: Center(
        child: Text('Details ID: $id'),
      ),
    );
  }
}

This code does the following:

  • It initializes the uni_links plugin and listens for incoming links using getInitialLink() and linkStream.
  • It checks if the app was started with a deep link by calling getInitialLink() in the initState() method.
  • It subscribes to the linkStream stream, which emits incoming links as strings.
  • The _handleDeepLink() method parses the link and navigates to the appropriate screen using Navigator.push().

Conclusion

Implementing deep linking in Flutter can significantly enhance the user experience and engagement with your app. Whether using the url_launcher package for simpler implementations or the uni_links package for more robust solutions, the process involves configuring the app manifest, listening for incoming links, and navigating accordingly. Deep linking provides a seamless way to connect external sources to specific content within your Flutter application.