Handling deep links in Flutter
By Ann Tech · 1 May 2025
Deep links open specific screens in your Flutter app from external URLs. They arrive from push notifications, QR codes, emails, other apps, and web browsers. Handling them correctly requires both native platform configuration and Flutter navigation logic.
Types of deep links
Custom schemes (myapp://orders/order-1): any app can register a custom scheme, so they're vulnerable to interception. Fine for internal use, not for security-sensitive flows.
Universal Links (iOS) / App Links (Android): HTTPS links that are verified to belong to your domain. Cannot be intercepted by other apps. Preferred for anything sensitive.
Deferred deep links: the user doesn't have the app installed. They're sent to the App Store/Play Store, install the app, then are taken to the intended screen on first launch.
Listening to incoming links
dependencies:
app_links: ^6.0.0 # Cross-platform deep link handling
class DeepLinkService {
final _appLinks = AppLinks();
StreamSubscription? _sub;
Future<void> init(GoRouter router) async {
// Handle link that launched the app (when app was terminated)
final initialLink = await _appLinks.getInitialLink();
if (initialLink != null) {
_navigate(router, initialLink);
}
// Handle links while app is running
_sub = _appLinks.uriLinkStream.listen(
(uri) => _navigate(router, uri),
onError: (e) => debugPrint('Deep link error: $e'),
);
}
void _navigate(GoRouter router, Uri uri) {
// Convert the incoming URI to a go_router path
final path = uri.path;
final query = uri.queryParameters;
if (path.startsWith('/orders/')) {
router.go(path);
} else if (path == '/reset-password') {
router.go('/auth/reset-password?token=${query['token']}');
} else {
router.go('/');
}
}
void dispose() => _sub?.cancel();
}
go_router with deep link support
go_router handles URI parsing for you:
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
GoRoute(
path: '/orders/:id',
builder: (_, state) => OrderDetailScreen(
orderId: state.pathParameters['id']!,
),
),
GoRoute(
path: '/auth/reset-password',
builder: (_, state) => ResetPasswordScreen(
token: state.uri.queryParameters['token'],
),
),
],
// Redirect unauthenticated deep links to login
redirect: (context, state) {
final isSignedIn = ref.read(isSignedInProvider);
if (!isSignedIn && !state.uri.path.startsWith('/auth')) {
final destination = Uri.encodeComponent(state.uri.toString());
return '/auth/login?redirect=$destination';
}
return null;
},
);
Android App Links configuration
android/app/src/main/AndroidManifest.xml:
<activity ...>
<!-- 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="myapp.example.com" />
</intent-filter>
<!-- Custom scheme fallback -->
<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>
</activity>
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:DD:..."]
}
}]
Get fingerprint: keytool -list -v -keystore your.keystore -alias your-alias
iOS Universal Links configuration
ios/Runner/Runner.entitlements:
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp.example.com</string>
</array>
https://myapp.example.com/.well-known/apple-app-site-association:
{
"applinks": {
"details": [{
"appIDs": ["TEAMID.com.example.myapp"],
"components": [
{"/#": ".*/orders/.*"},
{"/#": ".*/products/.*"},
{"/": "/share/*"}
]
}]
}
}
Deep links from Firebase Dynamic Links (deprecated)
Firebase Dynamic Links is deprecated (shutting down August 2025). Migrate to:
- Branch.io for deferred deep links and analytics
- AppsFlyer for attribution + deep links
- Or implement custom logic with App Links/Universal Links
Deep links from push notifications
// firebase_messaging
FirebaseMessaging.onMessageOpenedApp.listen((message) {
final link = message.data['deep_link'] as String?;
if (link != null) {
final uri = Uri.parse(link);
router.go(uri.path, extra: uri.queryParameters);
}
});
Testing
# Android
adb shell am start -a android.intent.action.VIEW \
-d 'https://myapp.example.com/orders/order-123'
# 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
Missing authentication guard. A deep link to /orders/order-1 should redirect to login if the user isn't authenticated, then return to the intended screen after login. Always add a redirect guard in go_router.
Not handling the initial link when the app is terminated. uriLinkStream only fires for links received while the app is running. getInitialLink() handles the case where the app was launched via a link from a terminated state.
Hosting the AASA/assetlinks file on a redirecting URL. Both Apple and Google require these files to be served without redirects. If https://example.com/.well-known/apple-app-site-association redirects to https://www.example.com/..., verification fails.
Sign in to like, dislike, or report.