
Flutter: Determining Deep Links & Universal Links Issues
Clicking a link opens the browser instead of your app? Master Android App Links (`assetlinks.json`), iOS Universal Links (`AASA`), and path handling with go_router.

Clicking a link opens the browser instead of your app? Master Android App Links (`assetlinks.json`), iOS Universal Links (`AASA`), and path handling with go_router.
Yellow stripes appear when the keyboard pops up? Learn how to handle layout overflows using resizeToAvoidBottomInset, SingleChildScrollView, and tricks for chat apps.

Push works on Android but silent on iOS? Learn to fix APNs certificates, handle background messages, configure Notification Channels, and debug FCM integration errors.

Think Android is easier than iOS? Meet Gradle Hell. Learn to fix minSdkVersion conflicts, Multidex limit errors, Namespace issues in Gradle 8.0, and master dependency analysis with `./gradlew dependencies`.

App crashes only in Release mode? It's likely ProGuard/R8. Learn how to debug obfuscated stack traces, use `@Keep` annotations, and analyze `usage.txt`.

You sent a promo SMS: https://myapp.com/event/123.
User clicks it. They have the app installed.
Expectation: The app opens directly to the event page.
Reality: Safari opens the web version (or a 404 page).
User thinks: "This app adds friction," and leaves. Deep linking is the bridge between the web and your app. If the bridge is broken, traffic falls into the river.
Gone are the days of insecure Custom Schemes (myapp://). We now use Universal Links (iOS) and App Links (Android).
The core concept is Domain Verification.
Your website must host a specific file that says: "Yes, I own the app with Bundle ID com.example.app. It is safe to open me." without user confirmation.
If you don't host these files correctly, nothing you do in the app code will matter.
assetlinks.json)Location: https://yourdomain.com/.well-known/assetlinks.json
Content: A JSON mapping your package name to your SHA-256 Fingerprint.
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": ["AE:89:your:debug:and:prod:sha256:keys"]
}
}]
Note: Make sure to verify the SHA-256 for BOTH Debug and Release keystores.
apple-app-site-association)Location: https://yourdomain.com/.well-known/apple-app-site-association
Content: JSON (without .json extension in URL).
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAMID.com.example.myapp",
"paths": [ "/event/*", "/user/*" ]
}
]
}
}
Server Requirement: The file MUST be served with Content-Type: application/json and must be accessible via HTTPS without redirects.
Now inform the OS that the app claims the domain.
AndroidManifest.xml)The autoVerify="true" attribute is the magic switch. It bypasses the "Open with..." dialog.
<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>
Signing & Capabilities.+ Capability -> Associated Domains.applinks:myapp.com (No https, no paths).go_router)Flutter receives the URL string. You need to parse it and navigate.
go_router handles standard URL path matching out of the box.
final goRouter = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
routes: [
GoRoute(
path: 'event/:id',
builder: (context, state) {
// Extracts '123' from '/event/123'
final id = state.pathParameters['id'];
return EventPage(id: id);
},
),
],
),
],
);
No manual regex parsing required. It just works.
What if the user doesn't have the app?
event/123.Standard Deep Links can't do this (context is lost during install). You need Deferred Deep Linking. Since Firebase Dynamic Links is sunsetting, you have two options:
If your deep link opens a page, but the user feels "trapped" because the Back button closes the app or the Bottom Tab Bar is missing, you have a Navigation UX issue.
Use ShellRoute to wrap your deep-linked screens inside the main app scaffold.
ShellRoute(
builder: (context, state, child) {
// Keeps the BottomNavigationBar visible even on deep linked pages
return Scaffold(
body: child,
bottomNavigationBar: MainBottomNav(),
);
},
routes: [
GoRoute(path: '/event/:id', ...),
],
)
This ensures a consistent app experience, regardless of entry point.
Real-world apps have multiple environments: Dev, Staging, Prod. Your deep link domains should reflect this to prevent testers from opening the production app.
dev.myapp.commyapp.comiOS Configuration (Build Settings):
Use a User-Defined Setting like DEEP_LINK_DOMAIN in Build Settings.
Set it to dev.myapp.com in Debug config, and myapp.com in Release config.
In Associated Domains, use applinks:$(DEEP_LINK_DOMAIN).
Android Configuration (Manifest Placeholders):
Use manifestPlaceholders in build.gradle.
productFlavors {
dev {
manifestPlaceholders = [hostName: "dev.myapp.com"]
}
prod {
manifestPlaceholders = [hostName: "myapp.com"]
}
}
In AndroidManifest.xml, use ${hostName}.
This strictly separates your environments.
Never use Uri.parse(urlString) directly on user input or deep links. It throws an exception if the URL is malformed, causing your app to crash.
Instead, use Uri.tryParse(urlString).
final uri = Uri.tryParse(incomingDeepLink);
if (uri == null) {
print("Failed to parse deep link");
return;
}
// Proceed safely
Also, always check uri.scheme and uri.host before acting on it. Someone could send a malicious deep link like myapp://delete_account. Validating the authority is crucial.
myapp://. Old way. Insecure. User gets a scary prompt. Avoid.https://myapp.com. iOS standard. Secure because it requires server-side AASA file.assetlinks.json.If your deep links are still failing, follow this checklist. 99% of issues are one of these three:
assetlinks.json or Apple's AASA file is invalid JSON.
jsonlint.com. A single missing comma breaks everything.curl -I https://yourdomain.com/.well-known/apple-app-site-association. Ensure it says Content-Type: application/json. If it says text/plain or application/octet-stream, iOS will ignore it.301 Redirects are happening. It must hold a 200 OK status.Deep links are the entry door to your app. If the door is jammed, users won't force it open—they'll just walk away to a competitor's website.
Setting up Universal Links and App Links feels like "Black Magic" because it involves three parties:
If any one of these is misconfigured, it fails silently. By following the steps—Server Validation, Manifest/Project Setup, and GoRouter parsing—you can build a robust navigation system that feels native and magical to the user.
Don't leave the door locked. Welcome your users properly.