Using Packages like uni_links in Flutter

Deep linking is a powerful technique that enables you to direct users to specific content within your mobile application from outside sources, such as web pages, email campaigns, or social media posts. Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase, makes implementing deep linking relatively straightforward with the help of packages like uni_links. This blog post delves into how to use uni_links in a Flutter application to handle deep links effectively.

What is Deep Linking?

Deep linking allows users to navigate directly to a specific location within an application, bypassing the need for the user to manually navigate through the app’s interface. For instance, clicking on a link could directly open a product page in an e-commerce app or a specific article in a news app.

Why Use Deep Linking?

  • Improved User Experience: Direct users to exactly what they need, enhancing satisfaction.
  • Enhanced Marketing: Track campaigns more effectively by directing users to specific promotions.
  • Increased Engagement: Share direct links to content, making it easier for users to spread the word.

Overview of uni_links Package

The uni_links package in Flutter simplifies the process of handling deep links. It provides a unified interface for both Android and iOS, making it easier to implement deep linking functionality across platforms. With uni_links, you can listen for incoming links and parse them to navigate users to the relevant parts of your application.

How to Implement Deep Linking with uni_links in Flutter

Step 1: Add Dependency

Add the uni_links package to your pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.5.1 # Use the latest version

Then, run flutter pub get to install the package.

Step 2: Configure Native Platforms (Android & iOS)

Before using uni_links, you need to configure your Android and iOS projects to handle incoming deep links.

Android Configuration

Open the AndroidManifest.xml file (usually located in android/app/src/main/) and add an intent filter to your main activity.

<activity
    android:name=".MainActivity"
    android:launchMode="singleTask"> <!-- Important: Set launchMode to singleTask -->
    <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="example.com"/> <!-- Define your custom scheme and host -->
    </intent-filter>
</activity>

In the above code:

  • android:launchMode="singleTask" ensures that only one instance of your activity handles the deep link.
  • The <data> tag defines the URI scheme (myapp) and host (example.com) that your app will respond to.
iOS Configuration

In the ios/Runner/Info.plist file, add the following XML snippet:

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string> <!-- Define your custom scheme -->
        </array>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string> <!-- Use your app’s bundle identifier -->
    </dict>
</array>

Replace com.example.myapp with your app’s bundle identifier and myapp with your custom scheme.

Step 3: Implement Deep Link Handling in Flutter

In your Flutter application, use the uni_links package to listen for incoming links and handle them appropriately.

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String? _latestLink = 'Unknown';

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

  Future<void> _initUniLinks() async {
    // Attach a listener to the stream of coming links
    _handleIncomingLinks();

    // Get the link to start the app with
    _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(() {
        _latestLink = uri?.toString() ?? 'Unknown';
        // Handle the deep link data here
        _processDeepLink(uri);
      });
    }, onError: (Object err) {
      if (!mounted) {
        return;
      }
      print('Got err: $err');
      setState(() {
        _latestLink = 'Failed to get latest link: $err.';
      });
    });
  }

  /// Handle the initial Uri - the one the app was started with
  ///
  /// **ATTENTION**: `getInitialUri`/`getInitialLink` should be handled
  /// ONLY ONCE in your app's lifetime, since it is not meant to change
  /// throughout the app's life.
  Future<void> _handleInitialUri() async {
    // In this example app, we let the FutureBuilder take care of the
    // handling, but it could be done in the same way as
    // _handleIncomingLinks().
    try {
      final initialUri = await getInitialUri();
      if (initialUri == null) {
        print('No initial URI received');
      } else {
        print('Initial URI received: $initialUri');
        if (!mounted) {
          return;
        }
        setState(() {
          _latestLink = initialUri.toString();
          _processDeepLink(initialUri);
        });
      }
    } on PlatformException {
      // Platform messages may fail, so handle gracefully
      print('Failed to receive initial URI');
    } on FormatException catch (err) {
      if (!mounted) {
        return;
      }
      print('Malformed Initial URI received: $err');
      setState(() => _latestLink = 'Malformed Initial URI received: $err');
    }
  }

  void _processDeepLink(Uri? uri) {
    if (uri != null) {
      // Extract data from the URI and navigate accordingly
      String? path = uri.pathSegments.isNotEmpty ? uri.pathSegments.first : null;
      String? param1 = uri.queryParameters['param1'];

      if (path == 'product') {
        // Navigate to the product details page
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => ProductDetailsPage(productId: param1 ?? 'default'),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('UniLinks Example App'),
        ),
        body: Center(
          child: Text('Latest link received: $_latestLink.'),
        ),
      ),
    );
  }
}

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'),
      ),
    );
  }
}

In the above example:

  • The _initUniLinks() method is called in initState() to initialize deep linking.
  • _handleIncomingLinks() listens for incoming links while the app is running.
  • _handleInitialUri() retrieves the initial link that the app was launched with.
  • _processDeepLink(Uri? uri) extracts relevant information from the URI and navigates the user to the appropriate screen.

Step 4: Test Your Deep Linking Implementation

To test deep linking:

  • On Android, use the adb command to simulate opening a deep link:
    adb shell am start -W -a android.intent.action.VIEW -d "myapp://example.com/product?param1=123" com.example.myapp
  • On iOS, you can use the xcrun simctl openurl command:
    xcrun simctl openurl booted "myapp://example.com/product?param1=123"

Best Practices for Using Deep Linking

  • Handle Errors: Gracefully handle cases where the deep link is malformed or invalid.
  • Provide Fallbacks: Ensure users are directed to a meaningful location if the deep link fails.
  • Secure Deep Links: Implement validation to prevent malicious use.

Conclusion

Deep linking with the uni_links package in Flutter is a robust way to improve user engagement and enhance the overall experience of your application. By following the steps outlined in this guide, you can effectively implement deep linking across both Android and iOS platforms, directing users to the exact content they need.