In a Flutter app, why prefer Android App Links and iOS Universal Links over a custom scheme like ticketapp:// for shared links?
answer
- who is allowed to claim the link
- no fallback when the app is missing
- https links work as web pages
- custom schemes need no hosted file
- go_router matches the path only
basics
~20 sA custom scheme like ticketapp:// needs no hosted file, but any app can claim it and it fails when the app is missing; verified https links are tied to your domain and fall back to the web page.
solid answer
~40 sA custom scheme is declared by the app alone: an intent filter with `<data android:scheme="ticketapp" .../>` on Android, or `CFBundleURLTypes` in `Info.plist` on iOS. That makes it easy, but nothing proves ownership, so another app can register the same scheme, and when the ticketing app is not installed the link simply fails; many apps also do not render it as a tappable link. App Links and Universal Links use ordinary `https://tickets.example.com/events/42` URLs that the domain verifies through `assetlinks.json` or `apple-app-site-association`, so only your app opens them and people without the app see the event page on the web. Custom schemes still suit app-to-app callbacks, such as returning from a sign-in flow. One Flutter-specific trap: go_router matches only the URI path, so in `ticketapp://event/42` the word `event` is the host and the route sees `/42`.
code
dart · 10 linesvoid main() {
final Uri custom = Uri.parse('ticketapp://events/42');
print('${custom.host} ${custom.path}'); // events /42
final Uri fixed = Uri.parse('ticketapp://open/events/42');
print('${fixed.host} ${fixed.path}'); // open /events/42
final Uri https = Uri.parse('https://tickets.example.com/events/42');
print(https.path); // /events/42
}go deeper
Recall that custom schemes need only app configuration while App Links and Universal Links use https URLs verified by a file on your domain.
Explain ownership, fallback when the app is absent, and how a custom scheme's host swallows the first path segment before go_router matches the path.
Choose verified links for anything shared with people, keep custom schemes for callbacks, and standardise the link format so web and app agree.
Set the organisation's link policy: domains, path ownership, fallback pages, and which flows may rely on custom schemes at all.
## Two ways to make a link open an app **Custom schemes** invent a URL scheme only the app understands: `ticketapp://events/42`. **Verified https links** use the real web address, `https://tickets.example.com/events/42`, and prove that the domain owner wants the app to handle it: App Links on Android, Universal Links on iOS. ## Declaring a custom scheme - **Android**: an intent filter on `MainActivity` with the `VIEW` action, the `DEFAULT` and `BROWSABLE` categories and `<data android:scheme="ticketapp" />`, with no `autoVerify` and no hosted file. - **iOS**: a `CFBundleURLTypes` entry in `ios/Runner/Info.plist` whose `CFBundleURLSchemes` array contains `ticketapp`. - **Flutter**: with the deep-linking flag at its default, the engine forwards the URL to the framework just like an https link, so the same router handles both. ## Comparing them | Property | Custom scheme `ticketapp://` | App Links / Universal Links `https://` | |---|---|---| | Setup | app configuration only | app configuration plus a hosted file on your domain | | Proof of ownership | none; any app can claim the scheme | the domain names your app and its signature or team | | App not installed | the link fails | the web page opens | | Shareable in chats and email | often not rendered as a link | an ordinary URL everyone can open | | Typical use | app-to-app callbacks, internal tools | shared content, campaigns, email links | For a shared event link, the last two rows decide it: people who receive the link may not have the app, and their tap should still show the event. ## The Flutter routing trap with custom schemes go_router matches only the URI's **path**. For an https link the host is the domain, so the path is exactly what you expect: `https://tickets.example.com/events/42` is matched as `/events/42`. For a custom scheme, the part right after `//` is also parsed as the **host**: 1. `ticketapp://events/42` has host `events` and path `/42`. 2. go_router looks for a route matching `/42`, finds none, and shows its error screen. 3. The link seemed fine because it looked like a path. Two fixes are common: - Put a fixed host in every custom-scheme link and keep the route in the path: `ticketapp://open/events/42` has the path `/events/42`. - Or use a triple slash, `ticketapp:///events/42`, which has an empty host and the path `/events/42`. Whichever is chosen, write it down once and generate links from code, so the web team and the app agree on the format. ## Handling both link kinds in one router An app often supports both: https links for sharing and a custom scheme for a payment or sign-in callback. With Flutter's deep-linking flag enabled, both reach the same `Router`, and go_router matches both by path, so one route table serves them if the formats agree: - `https://tickets.example.com/events/42` has the path `/events/42`. - `ticketapp://open/events/42` also has the path `/events/42`. - A callback such as `ticketapp://open/payment/complete?order=981` maps to its own route, read with `state.uri.queryParameters`. Because the router ignores scheme and host, it cannot tell the two apart by itself. If a route must accept only one kind, for example a callback route that must never be triggered by a web link, inspect `state.uri`, which keeps the incoming scheme and host, in a route-level redirect and send anything else to a safe page. ## When a custom scheme is still right - **Callbacks between apps**, such as returning from an external sign-in or payment page to the app, where the caller knows the app is installed. - **Internal or development tools** where hosting an association file is impractical. - As a **fallback** during development before the domain's files are published. ## Security note Because anyone can register a custom scheme, never put secrets such as tokens in a custom-scheme link and never trust its parameters without validation. The same caution applies to query parameters on verified links: verification proves which app opens the link, not that the link's content is safe.
- In a Flutter app using go_router, which path does ticketapp://events/42 produce, and why does it not match '/events/:id'?The URI parser treats `events` as the host, so the path is `/42`. go_router matches only the path, finds no route for `/42`, and shows its error screen. Using `ticketapp://open/events/42` or `ticketapp:///events/42` puts `/events/42` in the path.
- How do you declare a custom URL scheme for the iOS side of a Flutter app?Add a `CFBundleURLTypes` entry to `ios/Runner/Info.plist` whose `CFBundleURLSchemes` array lists the scheme, such as `ticketapp`. No hosted file or entitlement is needed, which is exactly why nothing proves that only your app claims it.
- When is a custom scheme still the better choice in a Flutter app?For app-to-app callbacks where the app is known to be installed, such as returning from an external sign-in or payment page, and for internal tools where hosting an association file is impractical.
saying these in an interview costs you the question
- Believes a custom scheme is verified and can only open your app.
- Expects a custom-scheme link to show a web page when the app is missing.
- Assumes ticketapp://events/42 reaches go_router with the path /events/42.
- Thinks App Links and Universal Links need no file on the domain.
- Puts access tokens in custom-scheme links shared between apps.