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:
- The OS receives a link tap and checks if an app is registered to handle it
- If your app is registered, the OS launches it (or brings it to the foreground)
- Flutter's engine receives the URL from the platform channel
- Your router (go_router, auto_route, or the built-in
Router) matches the URL to a route - 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.
- Select the Runner target
- Go to Signing & Capabilities
- Add the "Associated Domains" capability
- 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.
Digital Asset Links
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:
- Paste
https://go.yourapp.com/product/123in Notes and tap it - Send the link via iMessage and tap it
- 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
Links Open in Browser Instead of App
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)
FlutterDeepLinkingEnabledis set totruein 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_enabledmetadata is set totrue
Route Not Matched
If the app opens but shows the wrong screen (or the error/404 screen):
- Enable
debugLogDiagnosticson 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/123and/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
FlutterDeepLinkingEnabledplist key - On Android, verify the
flutter_deeplinking_enabledmetadata - If using
app_links, ensuregetInitialLink()is called before any navigation occurs
Production Checklist
- Associated Domains entitlement configured in Xcode
-
FlutterDeepLinkingEnabledset totruein Info.plist - AASA file accessible and valid
- Android intent filters added with
autoVerify="true" -
flutter_deeplinking_enabledmetadata set totrue - 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.