Back to Blog
Implementation
8 min read
February 10, 2024

How to Set Up Universal Links in Flutter (iOS)

Master Universal Links for iOS with this complete step-by-step guide. Learn Xcode configuration, AASA file setup, and Dart implementation for seamless deep linking.

What are Universal Links?

Definition

Universal Links are standard HTTPS URLs that iOS recognizes and routes directly to your app when it's installed. They provide a seamless experience where users never see an app chooser dialog and can be opened on both web and mobile without app-specific URL schemes.

Key Advantages

  • • No app chooser dialog
  • • Works as web links too
  • • Native iOS integration
  • • Better user experience
  • • SEO friendly
  • • Fallback to web version

How They Work

  • • User clicks an HTTPS link
  • • iOS checks Associated Domains
  • • iOS validates AASA file
  • • App opens if installed
  • • Fallback to web if not
  • • No manual routing needed

Xcode Configuration

Step 1: Add Associated Domains Entitlement

Open your Flutter project in Xcode and configure the Associated Domains capability:

1. Open ios/Runner.xcworkspace

2. Select the Runner project in the navigator

3. Select the Runner target

4. Go to Signing & Capabilities tab

5. Click + Capability button

6. Search for and add "Associated Domains"

Step 2: Configure Domain Entries

Add your domain to the Associated Domains list:

text
applinks:yourdomain.com
applinks:www.yourdomain.com

Note: Use applinks: prefix for Universal Links. You can add multiple domains if needed.

Step 3: Update Info.plist

Ensure your app includes the proper URL handling configuration:

xml
<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleURLName</key>
        <string>com.example.myapp</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>

AASA File Setup

What is AASA?

The Apple App Site Association (AASA) file is a JSON file that tells iOS which URLs should be handled by your app. It must be hosted at /.well-known/apple-app-site-association on your domain.

Step 1: Create the AASA File

Create a JSON file without a file extension:

json
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.myapp",
        "paths": [
          "/product/*",
          "/user/*",
          "/share/*"
        ]
      }
    ]
  }
}

Step 2: Find Your Team ID

You need your Apple Team ID to create the appID. You can find it:

1. Go to developer.apple.com

2. Sign in with your Apple Developer account

3. Go to Account → Membership

4. Copy your Team ID (10-character code)

Step 3: Upload AASA File

Host the file on your web server at the correct path:

text
https://yourdomain.com/.well-known/apple-app-site-association

Tip: Validate your AASA file using our AASA Validator tool to ensure it's correctly formatted.

Dart Code Implementation

Step 1: Add uni_links Package

Add the uni_links package to your pubspec.yaml:

yaml
dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.9.0

Step 2: Create Deep Link Handler

Implement a handler for incoming Universal Links:

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

class UniversalLinkHandler {
  static StreamSubscription? _deepLinkSubscription;

  static Future<void> initialize(BuildContext context) async {
    // Handle link when app is opened from terminated state
    final String? initialLink = await getInitialLink();
    if (initialLink != null) {
      _handleDeepLink(initialLink, context);
    }

    // Listen for deep links when app is running
    _deepLinkSubscription = linkStream.listen(
      (String link) {
        _handleDeepLink(link, context);
      },
      onError: (err) {
        debugPrint('Deep link error: $err');
      },
    );
  }

  static void _handleDeepLink(String link, BuildContext context) {
    final Uri uri = Uri.parse(link);

    debugPrint('Received deep link: $link');
    debugPrint('Scheme: ${uri.scheme}');
    debugPrint('Host: ${uri.host}');
    debugPrint('Path: ${uri.path}');

    // Route based on path
    if (uri.pathSegments.isNotEmpty) {
      final String firstSegment = uri.pathSegments[0];

      switch (firstSegment) {
        case 'product':
          if (uri.pathSegments.length > 1) {
            final String productId = uri.pathSegments[1];
            _navigateToProduct(context, productId);
          }
          break;
        case 'user':
          if (uri.pathSegments.length > 1) {
            final String userId = uri.pathSegments[1];
            _navigateToUser(context, userId);
          }
          break;
        case 'share':
          if (uri.pathSegments.length > 1) {
            final String shareCode = uri.pathSegments[1];
            _handleShare(context, shareCode);
          }
          break;
        default:
          _navigateToHome(context);
      }
    }
  }

  static void _navigateToProduct(BuildContext context, String productId) {
    debugPrint('Navigate to product: $productId');
    // Implementation: Navigate to product screen with productId
    // Example: Navigator.of(context).pushNamed('/product', arguments: productId);
  }

  static void _navigateToUser(BuildContext context, String userId) {
    debugPrint('Navigate to user: $userId');
    // Implementation: Navigate to user profile screen
  }

  static void _handleShare(BuildContext context, String shareCode) {
    debugPrint('Handle share code: $shareCode');
    // Implementation: Handle referral or share code
  }

  static void _navigateToHome(BuildContext context) {
    debugPrint('Navigate to home');
    // Implementation: Navigate to home screen
  }

  static void dispose() {
    _deepLinkSubscription?.cancel();
  }
}

Step 3: Initialize in Main App

Call the handler in your main widget:

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

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

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);

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

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    // Initialize deep link handler after widget binding
    WidgetsBinding.instance.addPostFrameCallback((_) {
      UniversalLinkHandler.initialize(context);
    });
  }

  @override
  void dispose() {
    UniversalLinkHandler.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      home: const HomeScreen(),
    );
  }
}

Testing Universal Links

Test on Real Device

Testing on a physical iOS device is essential since the simulator has limitations:

1. Build your app to a real iOS device

2. Open Notes or Safari app

3. Type or paste your Universal Link: https://yourdomain.com/product/123

4. Long-press the link and tap "Open in App"

5. App should open and navigate to the correct screen

Using Apple's Validation Tool

Check if your AASA file is properly hosted:

bash
curl -I https://yourdomain.com/.well-known/apple-app-site-association

Expected response: HTTP/1.1 200 OK

Simulator Limitations

The iOS Simulator cannot properly test Universal Links due to sandbox restrictions. Always test on a real device with a valid Apple Developer account and properly signed build.

Common Issues & Fixes

Issue: Link Opens in Safari Instead of App

Cause: AASA file not found or improperly configured

Solution:

  • • Verify AASA file exists at /.well-known/apple-app-site-association
  • • Check AASA JSON syntax using our validator
  • • Ensure appID matches your bundle ID and Team ID
  • • Check domain is listed in Associated Domains entitlement

Issue: Deep Link Not Received by App

Cause: Deep link handler not properly initialized

Solution:

  • • Initialize handler in main.dart with proper context
  • • Add debug prints to verify handler is called
  • • Check that uni_links package is properly added
  • • Ensure app is fully launched before testing

Issue: Associated Domains Entitlement Missing

Cause: Capability not added in Xcode

Solution:

  • • Re-open Xcode project
  • • Go to Signing & Capabilities tab
  • • Verify Associated Domains is present
  • • Re-add if missing and rebuild

Ready for Android?

Once you've mastered Universal Links for iOS, implement the Android equivalent using App Links. Learn how to set up App Links in our complete Android guide.

Related Articles