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.
How deep links work
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,
)
iOS Universal Links setup
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.
Android App Links setup
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
Custom scheme deep links
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>
Handling deep links from push notifications
// 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);
});
}
}
Testing deep links
# 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.