← Articles

Deep linking with go_router in Flutter

By Charlin Joe · 14 May 2025

Deep links open specific screens in your app from external URLs — emails, push notifications, QR codes, other apps. go_router handles deep links cleanly with its URL-based routing.

When a user taps a link like https://myapp.example.com/orders/order-1, the OS routes it to your app (if installed), which maps the URL to a specific screen.

There are two kinds:

  • Custom scheme (myapp://orders/order-1): simpler but not secure — any app can register the same scheme
  • Universal links (iOS) / App links (Android): verified HTTPS links — requires hosting a file on your domain

go_router setup

dependencies:
  go_router: ^14.0.0
final router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (_, __) => const HomeScreen(),
    ),
    GoRoute(
      path: '/orders',
      builder: (_, __) => const OrdersScreen(),
      routes: [
        GoRoute(
          path: ':orderId',
          builder: (_, state) => OrderDetailScreen(
            orderId: state.pathParameters['orderId']!,
          ),
        ),
      ],
    ),
    GoRoute(
      path: '/products/:productId',
      builder: (_, state) {
        final productId = state.pathParameters['productId']!;
        final tab = state.uri.queryParameters['tab'];
        return ProductScreen(productId: productId, initialTab: tab);
      },
    ),
  ],
);

In MaterialApp:

MaterialApp.router(
  routerConfig: router,
)

Step 1: Add the Associated Domains entitlement:

<!-- ios/Runner/Runner.entitlements -->
<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:myapp.example.com</string>
</array>

Step 2: Host the Apple App Site Association file:

// https://myapp.example.com/.well-known/apple-app-site-association
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAMID.com.example.myapp",
        "paths": ["/orders/*", "/products/*", "/share/*"]
      }
    ]
  }
}

Serve it with Content-Type: application/json and no redirect.

Step 1: Add intent filters in AndroidManifest.xml:

<activity ...>
  <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="myapp.example.com"
          android:pathPrefix="/orders" />
  </intent-filter>

  <intent-filter android:autoVerify="true">
    <data android:scheme="https"
          android:host="myapp.example.com"
          android:pathPrefix="/products" />
  </intent-filter>
</activity>

Step 2: Host the Digital Asset Links file:

// https://myapp.example.com/.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.myapp",
    "sha256_cert_fingerprints": [
      "AA:BB:CC:..."
    ]
  }
}]

Get your SHA-256 fingerprint:

keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android
# For production:
keytool -list -v -keystore your-release-key.jks

For simpler setups or marketing links that open the app if installed, fall back to the web otherwise:

<!-- AndroidManifest.xml -->
<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" />
</intent-filter>
<!-- ios/Runner/Info.plist -->
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.example.myapp</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>myapp</string>
    </array>
  </dict>
</array>
// firebase_messaging setup
FirebaseMessaging.onMessageOpenedApp.listen((message) {
  final link = message.data['deep_link'] as String?;
  if (link != null) {
    router.go(Uri.parse(link).path);
  }
});

// App opened from terminated state
final initialMessage = await FirebaseMessaging.instance.getInitialMessage();
if (initialMessage != null) {
  final link = initialMessage.data['deep_link'] as String?;
  if (link != null) {
    // Schedule navigation after app is fully initialized
    WidgetsBinding.instance.addPostFrameCallback((_) {
      router.go(Uri.parse(link).path);
    });
  }
}
# Android
adb shell am start -a android.intent.action.VIEW \
  -d 'https://myapp.example.com/orders/order-123' \
  com.example.myapp

# iOS simulator
xcrun simctl openurl booted 'https://myapp.example.com/orders/order-123'

# Custom scheme
adb shell am start -a android.intent.action.VIEW -d 'myapp://orders/order-123'
xcrun simctl openurl booted 'myapp://orders/order-123'

Common pitfalls

Not handling the case where the user isn't logged in. A deep link to /orders/order-1 should redirect to login if the user isn't authenticated, then return to the deep link destination after login. go_router's redirect parameter handles this:

GoRouter(
  redirect: (context, state) {
    final isLoggedIn = ref.read(authProvider).isLoggedIn;
    if (!isLoggedIn && !state.uri.path.startsWith('/auth')) {
      return '/auth/login?redirect=${state.uri}';
    }
    return null;
  },
)

AASA file not served correctly. Apple's CDN aggressively caches the AASA file. After updating it, wait up to 24 hours or test on a fresh install. Also verify it's served without a redirect and with Content-Type: application/json.

Sign in to like, dislike, or report.

Deep linking with go_router in Flutter — ANN Tech