Implementing Custom Logic for Handling Incoming Deep Links in Flutter

Deep linking is a powerful mechanism that allows users to navigate directly to a specific section within your Flutter application from an external source, such as a website, email, or another app. While Flutter provides built-in support for deep links, sometimes you need more control and customization to handle these links effectively. This blog post delves into how to implement custom logic for handling incoming deep links in Flutter.

What are Deep Links?

Deep links are URIs (Uniform Resource Identifiers) that route users to a specific location within an application. They provide a seamless user experience by eliminating the need for users to navigate through multiple screens to reach the desired content.

Why Use Custom Logic for Handling Deep Links?

  • Complex Navigation: Handles intricate navigation flows based on deep link parameters.
  • Dynamic Routing: Adapts to changes in your app’s structure without requiring updates to external links.
  • Custom Parameters: Parses and processes additional data sent via the deep link.
  • Conditional Logic: Applies business logic to determine the destination screen based on the user’s state or app settings.

How to Implement Custom Logic for Handling Deep Links in Flutter

Implementing custom logic for handling deep links involves setting up the necessary configurations and writing the code to process incoming links.

Step 1: Configure Your Flutter App for Deep Linking

First, you need to configure your Flutter app to handle deep links. This involves modifying the AndroidManifest.xml (for Android) and Info.plist (for iOS) files.

Android Configuration (AndroidManifest.xml)

Add an intent filter to your AndroidManifest.xml within the <activity> tag:

<activity
    android:name=".MainActivity"
    android:launchMode="singleTask"> <!-- Important for handling deep links properly -->
    <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" /> <!-- Change to your scheme and host -->
    </intent-filter>
</activity>

Explanation:

  • android:scheme: The URI scheme for your app (e.g., myapp).
  • android:host: The URI host for your app (e.g., open).
  • android:launchMode="singleTask": Ensures that the app reuses an existing instance when opened via a deep link.
iOS Configuration (Info.plist)

Add the following configuration to your Info.plist file:

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

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

Step 2: Implement Deep Link Handling in Flutter

Use the uni_links package to listen for incoming deep links. Add it to your pubspec.yaml:

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

Install the package by running flutter pub get.

Step 3: Write Custom Logic to Handle Incoming Links

Implement custom logic to parse and process incoming deep links.

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 {
  String? _initialLink;
  String? _latestLink;

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

  Future _initDeepLinkHandling() async {
    // Get the initial link, if any
    try {
      _initialLink = await getInitialLink();
      if (_initialLink != null) {
        _handleDeepLink(_initialLink!);
      }
    } on PlatformException {
      _initialLink = 'Failed to get initial link.';
    } catch (e) {
      _initialLink = 'An error occurred: $e';
    }

    // Listen for subsequent links
    uriLinkStream.listen((Uri? uri) {
      if (uri != null) {
        _latestLink = uri.toString();
        _handleDeepLink(_latestLink!);
      }
    }, onError: (err) {
      _latestLink = 'Failed to get latest link: $err.';
    });

    if (!mounted) return;

    setState(() {});
  }

  void _handleDeepLink(String link) {
    final uri = Uri.parse(link);
    if (uri.scheme == 'myapp' && uri.host == 'open') {
      // Custom logic to navigate based on path and parameters
      String? route = uri.pathSegments.isNotEmpty ? uri.pathSegments.first : null;
      Map params = uri.queryParameters;

      switch (route) {
        case 'product':
          String? productId = params['id'];
          if (productId != null) {
            // Navigate to product details page with productId
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => ProductDetailsPage(productId: productId)),
            );
          }
          break;
        case 'profile':
          String? userId = params['user'];
          if (userId != null) {
            // Navigate to user profile page with userId
            Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => UserProfilePage(userId: userId)),
            );
          }
          break;
        default:
          // Default navigation
          Navigator.push(
            context,
            MaterialPageRoute(builder: (context) => HomePage()),
          );
          break;
      }
    } else {
      // Handle other links or show an error
      print('Unhandled link: $link');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Deep Link Handling'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              Text('Initial Link: $_initialLinknLatest Link: $_latestLink'),
            ],
          ),
        ),
      ),
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home Page')),
      body: Center(child: Text('Welcome to the Home Page!')),
    );
  }
}

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

class UserProfilePage extends StatelessWidget {
  final String userId;
  UserProfilePage({required this.userId});

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

Explanation:

  • _initDeepLinkHandling():
    • Retrieves the initial link when the app starts.
    • Listens for subsequent links while the app is running.
  • _handleDeepLink(String link):
    • Parses the incoming URI.
    • Checks the scheme and host to ensure it’s a valid deep link for your app.
    • Extracts the route and parameters from the URI.
    • Implements custom navigation logic based on the extracted information.
    • Navigates to the appropriate page using Navigator.push.
  • The sample app includes dummy pages (HomePage, ProductDetailsPage, and UserProfilePage) to demonstrate the navigation.

Step 4: Testing Your Implementation

To test your implementation, use the following commands:

Android:
adb shell am start -W -a android.intent.action.VIEW -d "myapp://open/product?id=123" com.example.myapp
iOS:

You can simulate deep links in iOS using the xcrun command. First, get the app’s bundle identifier:

xcrun simctl openurl booted "myapp://open/product?id=123"

Best Practices for Handling Deep Links

  • Error Handling: Implement robust error handling to gracefully manage invalid or malformed links.
  • Security: Validate and sanitize deep link parameters to prevent security vulnerabilities.
  • Testing: Thoroughly test deep links on both Android and iOS devices.
  • Documentation: Clearly document your deep link schemes and supported parameters.
  • Fallback Navigation: Provide a fallback navigation strategy if the deep link cannot be processed.

Conclusion

Implementing custom logic for handling incoming deep links in Flutter enhances your app’s ability to respond dynamically to external requests. By configuring your app, using the uni_links package, and implementing custom navigation logic, you can create a seamless user experience. Follow the best practices to ensure your implementation is robust, secure, and well-documented.