Skip to content
Tolinku
Tolinku
Sign In Start Free
Engineering · · 6 min read

Flutter Deep Linking: Complete Setup Tutorial

By Tolinku Staff
|
Tolinku cross platform dashboard screenshot for engineering blog posts

Flutter handles deep linking through a combination of platform-specific configuration (iOS and Android) and Dart-level routing. This guide walks through the complete setup, from native configuration to handling URLs in your Flutter app.

For go_router-specific patterns, see Flutter go_router Deep Links. For a cross-platform overview, see Cross-Platform Deep Linking Guide.

How Flutter Deep Linking Works

Flutter's deep linking flow:

  1. The OS receives a link tap and checks if an app is registered to handle it
  2. If your app is registered, the OS launches it (or brings it to the foreground)
  3. Flutter's engine receives the URL from the platform channel
  4. Your router (go_router, auto_route, or the built-in Router) matches the URL to a route
  5. The matched screen is displayed

Flutter 3.x+ has built-in deep linking support through the Router widget and RouteInformationParser. The most popular routing package for deep linking is go_router.

Step 1: iOS Configuration

Associated Domains

Open ios/Runner.xcworkspace in Xcode.

  1. Select the Runner target
  2. Go to Signing & Capabilities
  3. Add the "Associated Domains" capability
  4. Add applinks:go.yourapp.com

Info.plist (Custom URL Scheme, Optional)

If you also want to support custom URL schemes, add to ios/Runner/Info.plist:

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>yourapp</string>
    </array>
    <key>CFBundleURLName</key>
    <string>com.yourapp</string>
  </dict>
</array>

FlutterDeepLinkingEnabled

Add this to your Info.plist to enable Flutter's built-in deep link handling:

<key>FlutterDeepLinkingEnabled</key>
<true/>

This tells the Flutter engine to automatically forward incoming URLs to your Dart router. Without this, you'd need to write platform channel code to pass URLs from the native side to Dart.

Step 2: Android Configuration

AndroidManifest.xml

Add intent filters to your main activity in android/app/src/main/AndroidManifest.xml:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTop">

  <!-- Deep Links (App Links) -->
  <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="go.yourapp.com" />
  </intent-filter>

  <!-- Existing metadata... -->
  <meta-data
    android:name="flutter_deeplinking_enabled"
    android:value="true" />
</activity>

The flutter_deeplinking_enabled metadata is the Android equivalent of the iOS FlutterDeepLinkingEnabled plist entry.

Ensure your domain serves a valid assetlinks.json at https://go.yourapp.com/.well-known/assetlinks.json. If using Tolinku, this is configured automatically in your Appspace settings.

Get your SHA-256 fingerprint:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey

For release builds with Google Play App Signing, get the fingerprint from Play Console (Setup > App signing).

Step 3: Flutter Routing with go_router

Install go_router

# pubspec.yaml
dependencies:
  go_router: ^14.0.0

Define Routes

import 'package:go_router/go_router.dart';

final router = GoRouter(
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/product/:id',
      builder: (context, state) {
        final productId = state.pathParameters['id']!;
        return ProductScreen(id: productId);
      },
    ),
    GoRoute(
      path: '/invite/:code',
      builder: (context, state) {
        final code = state.pathParameters['code']!;
        return InviteScreen(code: code);
      },
    ),
    GoRoute(
      path: '/promo/:slug',
      builder: (context, state) {
        final slug = state.pathParameters['slug']!;
        return PromoScreen(slug: slug);
      },
    ),
  ],
  errorBuilder: (context, state) => const NotFoundScreen(),
);

Use the Router

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

When a deep link arrives (e.g., https://go.yourapp.com/product/456), go_router automatically matches the path /product/456 to the ProductScreen route and extracts 456 as the id parameter.

Handling Query Parameters

For links with query parameters like https://go.yourapp.com/search?q=shoes&sort=price:

GoRoute(
  path: '/search',
  builder: (context, state) {
    final query = state.uri.queryParameters['q'] ?? '';
    final sort = state.uri.queryParameters['sort'] ?? 'relevance';
    return SearchScreen(query: query, sort: sort);
  },
),

Step 4: Handling the Full URL

By default, Flutter's deep link integration strips the scheme and host from the URL, passing only the path to your router. So https://go.yourapp.com/product/123 becomes /product/123.

If you need the full URL (for example, to distinguish between different domains), you can use the app_links package:

dependencies:
  app_links: ^6.0.0
import 'package:app_links/app_links.dart';

class DeepLinkHandler {
  final AppLinks _appLinks = AppLinks();

  void init() {
    // Listen for links while app is running
    _appLinks.uriLinkStream.listen((Uri uri) {
      handleLink(uri);
    });

    // Check for link that launched the app
    _appLinks.getInitialLink().then((Uri? uri) {
      if (uri != null) {
        handleLink(uri);
      }
    });
  }

  void handleLink(Uri uri) {
    // Full URL access: uri.host, uri.path, uri.queryParameters
    if (uri.host == 'go.yourapp.com') {
      router.go(uri.path);
    }
  }
}

Step 5: Testing

iOS Testing

On a physical device:

  1. Paste https://go.yourapp.com/product/123 in Notes and tap it
  2. Send the link via iMessage and tap it
  3. Verify the app opens and displays the product screen

To test during development without a configured domain, use the custom URL scheme:

yourapp://product/123

Android Testing

Use ADB:

adb shell am start -a android.intent.action.VIEW \
  -d "https://go.yourapp.com/product/123" \
  com.yourapp

Flutter DevTools

Flutter DevTools shows the current route state. Open DevTools, navigate to the "Router" tab, and trigger a deep link. You'll see the URL being parsed and the matched route.

Debug Logging

Add logging to your router to trace deep link handling:

final router = GoRouter(
  debugLogDiagnostics: true, // Logs route matching
  routes: [
    // ...
  ],
);

This prints route matching information to the console, which is useful for debugging why a link isn't matching the expected route.

Common Issues

iOS: Verify that:

  • The Associated Domains entitlement exactly matches your domain (including subdomain)
  • The AASA file is valid and accessible
  • You deleted and reinstalled the app after adding the entitlement (iOS caches AASA on install)
  • FlutterDeepLinkingEnabled is set to true in Info.plist

Android: Verify that:

  • The intent filter has android:autoVerify="true"
  • The assetlinks.json file is valid and accessible
  • The SHA-256 fingerprint matches your signing key
  • flutter_deeplinking_enabled metadata is set to true

Route Not Matched

If the app opens but shows the wrong screen (or the error/404 screen):

  • Enable debugLogDiagnostics on your GoRouter
  • Check that the path pattern matches the incoming URL
  • Remember that the scheme and host are stripped; only the path is matched
  • Check for trailing slashes: /product/123 and /product/123/ may be different routes

Cold Start Not Working

If deep links work when the app is backgrounded but not when it's fully closed:

  • On iOS, verify the FlutterDeepLinkingEnabled plist key
  • On Android, verify the flutter_deeplinking_enabled metadata
  • If using app_links, ensure getInitialLink() is called before any navigation occurs

Production Checklist

  • Associated Domains entitlement configured in Xcode
  • FlutterDeepLinkingEnabled set to true in Info.plist
  • AASA file accessible and valid
  • Android intent filters added with autoVerify="true"
  • flutter_deeplinking_enabled metadata set to true
  • assetlinks.json accessible and valid (both debug and release fingerprints)
  • go_router routes match all deep link paths
  • Error/404 route handles unmatched paths gracefully
  • Tested on physical iOS device
  • Tested on physical Android device
  • Cold start deep links work on both platforms

For integrating with the Tolinku SDK, see the Flutter SDK documentation. For advanced routing patterns, see Deep Link Routing Guide.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.