Deep Linking and Platform Configuration
Deep Linking and Platform Configuration
Deep linking allows external URLs to open specific screens inside your Flutter app. A user tapping a link like myapp://profile/42 or https://myapp.com/products/99 lands directly on the relevant screen rather than the home page. This lesson covers how to configure both Android and iOS for deep links, wire those links into GoRouter, and test the full flow on real devices and emulators.
What Is a Deep Link?
A deep link is a URI that routes a user into a specific location within a mobile app. There are three common deep-link flavours in Flutter:
- Custom-scheme URIs — e.g.
myapp://products/42. Simple to configure but not clickable on the web. - Android App Links — HTTPS URLs verified against your domain via Digital Asset Links. Android opens the app instead of the browser when verification passes.
- iOS Universal Links — HTTPS URLs associated with your domain via an
apple-app-site-association(AASA) file. iOS routes verified links directly to the app.
Android Configuration — Intent Filters
Android uses intent filters declared in AndroidManifest.xml to describe which URIs the app can handle. Open android/app/src/main/AndroidManifest.xml and add the filter inside your main <activity> element.
android/app/src/main/AndroidManifest.xml — custom scheme + App Links
<!-- Inside <activity android:name=".MainActivity" ...> -->
<!-- Custom scheme: myapp://... -->
<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>
<!-- Android App Links: https://myapp.com/... -->
<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.com" />
</intent-filter>
The android:autoVerify="true" attribute tells Android to verify ownership of myapp.com by fetching https://myapp.com/.well-known/assetlinks.json. That file must list your app's SHA-256 certificate fingerprint.
iOS Configuration — URL Schemes and Associated Domains
For iOS, two things must be configured in Xcode (or directly in the source files):
- Custom URL scheme — add a
CFBundleURLTypesentry toios/Runner/Info.plist. - Universal Links — enable the Associated Domains capability and add
applinks:myapp.com.
ios/Runner/Info.plist — custom URL scheme
<!-- Add inside the root <dict> -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
For Universal Links, open Xcode → Runner → Signing & Capabilities → + Capability → Associated Domains and add applinks:myapp.com. This generates an entitlements entry. The AASA file must be hosted at https://myapp.com/.well-known/apple-app-site-association.
flutter_deep_link or go_router package documentation links to Apple's and Google's validators. Use the App Links Assistant in Android Studio and Apple's AASA Validator at branch.io/resources/aasa-validator to check your server files before shipping.Handling Deep Links with GoRouter
GoRouter integrates with Flutter's Router API which automatically receives deep-link URIs from the platform. You only need to ensure your route paths match the incoming URI segments. GoRouter's initialLocation sets the default screen when the app cold-starts without a deep link.
main.dart — GoRouter with deep-link-ready routes
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final _router = GoRouter(
initialLocation: '/home', // Default on cold start
debugLogDiagnostics: true, // Log all navigation events
routes: [
GoRoute(
path: '/home',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/products/:id', // :id matches the path segment
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductDetailScreen(productId: id);
},
),
GoRoute(
path: '/profile/:userId',
builder: (context, state) {
final userId = state.pathParameters['userId']!;
final tab = state.uri.queryParameters['tab'] ?? 'posts';
return ProfileScreen(userId: userId, initialTab: tab);
},
),
],
);
void main() {
runApp(
MaterialApp.router(
routerConfig: _router,
title: 'Deep Link Demo',
),
);
}
When Android fires the intent https://myapp.com/products/99, Flutter's engine strips the host and passes /products/99 to GoRouter, which matches the /products/:id route and builds ProductDetailScreen(productId: '99'). Query parameters are accessible via state.uri.queryParameters.
Redirects and Authentication Guards
Deep links bypass your normal navigation flow, so you must guard authenticated routes. GoRouter's redirect callback runs before any route is built, making it the correct place to enforce auth:
GoRouter redirect — protecting deep-linked routes
final _router = GoRouter(
initialLocation: '/home',
redirect: (BuildContext context, GoRouterState state) {
final isLoggedIn = AuthService.instance.isLoggedIn;
final isOnLogin = state.matchedLocation == '/login';
// Not logged in and not heading to login → send to login
if (!isLoggedIn && !isOnLogin) return '/login';
// Already logged in and heading to login → send home
if (isLoggedIn && isOnLogin) return '/home';
// No redirect needed
return null;
},
routes: [
GoRoute(path: '/login', builder: (c, s) => const LoginScreen()),
GoRoute(path: '/home', builder: (c, s) => const HomeScreen()),
GoRoute(
path: '/orders/:id',
builder: (c, s) => OrderScreen(orderId: s.pathParameters['id']!),
),
],
);
/orders/123 will render the order screen even when the user is logged out. Always validate auth state (and other preconditions) in the redirect callback for every protected route.Testing Deep Links
You can trigger deep links on emulators and physical devices from the command line without a browser:
- Android (adb):
adb shell am start -a android.intent.action.VIEW -d "myapp://products/42" com.example.myapp - iOS (xcrun):
xcrun simctl openurl booted "myapp://products/42" - Flutter DevTools: The Deep Links tab (Flutter 3.19+) validates your manifest entries and lets you fire test URIs directly.
debugLogDiagnostics: true on your GoRouter instance during development. Every incoming URI, matched route, and redirect is printed to the console, making it trivial to trace why a link landed on the wrong screen.Summary
Deep linking connects the web and native worlds. On Android, intent filters in AndroidManifest.xml declare which URIs open your app; App Links add domain verification for HTTPS links. On iOS, Info.plist registers custom schemes and Associated Domains enables Universal Links. GoRouter handles incoming URIs automatically — your only job is to ensure path patterns match and that redirect guards protect sensitive routes. Always test with adb or xcrun simctl before shipping.