Diagnosing Xamarin.Forms Shell Deep‑Link Failures: A Practical Step‑by‑Step Guide
When a Xamarin.Forms Shell app opens from a custom scheme or Universal Link but lands on the wrong page or throws a route error, this guide shows a step‑by‑step diagnostic process—route registration, intent‑filter, platform checks, and how to verify each step.
14 Jun 2026, 12:27 UTC

Recognized Problem
When a user opens a custom scheme (e.g., myapp://items/42) or a Universal Link (e.g., https://example.com/items/42), the app starts but either lands on the wrong page, throws a Route not found exception, drops query parameters, or duplicates the root page on the navigation stack. The symptoms are consistent across Android and iOS, yet the underlying causes differ.
Diagnostic Table
| Cause | Typical Symptoms | Key Check |
|---|---|---|
Missing route registration in AppShell.xaml.cs | "Route not found" exception | Verify Routing.RegisterRoute("items/{id}", typeof(DetailPage)); |
| Route template mismatch (e.g., ‘//details/{id}’ vs ‘//details’) | Parameter not parsed, page shows defaults | Check template string matches navigation URI |
| Android intent‑filter mis‑configured | App opens in browser, no navigation | Inspect AndroidManifest.xml for android:autoVerify="true" and host/pathPrefix |
| iOS Associated Domains missing or broken | Safari opens, no app launch | Verify Associated Domains entitlement and HTTPS host |
Double initialization of AppShell | Duplicate root page, navigation stack corruption | Check Application.Current.MainPage only set once |
| Linker stripping route factories | Navigation fails in Release builds | Use [Preserve(AllMembers = true)] on page classes |
| Unencoded query parameters | Parameters lost or malformed | Percent‑encode values before GoToAsync |
Ordered Checks and Fixes
- Confirm Route Registration
Open
AppShell.xaml.csand ensure every deep‑link target has a correspondingRouting.RegisterRoutecall. Example for a detail page:public AppShell() { InitializeComponent(); Routing.RegisterRoute("items/{id}", typeof(DetailPage)); }Run the app and trigger
Shell.Current.GoToAsync("items/42");from a test button. If you see aRoute not foundexception, the registration is missing or incorrect. - Validate Route Template
Ensure the template string matches the URI you send. A common mistake is adding an extra slash or omitting the parameter placeholder. For example,
//details/{id}will not match//details. Use theShell.Current.CurrentState.Location.OriginalStringproperty after navigation to verify the resolved URI. - Check Android Intent‑Filter
In
AndroidManifest.xmllocate theintent-filterfor yourMainActivity:<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="https" android:host="example.com" android:pathPrefix="/items" /> </intent-filter>For Android 12+, add
android:exported="true"to theactivityelement. If the intent‑filter is missingandroid:autoVerify="true", the OS may open the link in a browser instead of the app. Verify withadb logcatthat the intent is received by the activity. - Verify iOS Universal Links
In Xcode, ensure the
Associated Domainsentitlement contains the correct host:applinks:example.comAlso, the
apple-app-site-associationJSON must be served over HTTPS with a valid TLS certificate and contain thepathsfield matching your deep‑link URL. If Safari opens the link instead of the app, the entitlement or host is incorrect. - Prevent Double Shell Instantiation
Only set
Application.Current.MainPageonce, typically inApp.xaml.cs. Avoid creating a newAppShellinMainActivity.OnCreateorAppDelegate.FinishedLaunching. Duplicate shells corrupt the navigation stack and can lead to duplicate root pages. - Address Linker Trimming
In Release builds with
Link All, Xamarin’s linker may strip parameterless constructors used by Shell’s route factories. Apply the[Preserve(AllMembers = true)]attribute to page classes:[Preserve(AllMembers = true)] public partial class DetailPage : ContentPage { public DetailPage() { InitializeComponent(); } }Alternatively, set the linker behavior to
Link SDK Onlyfor a quick workaround. - Encode Query Parameters
Shell does not auto‑encode query strings. If you need to pass complex values, percent‑encode them before calling
GoToAsync:var encoded = Uri.EscapeDataString("value with spaces & symbols"); await Shell.Current.GoToAsync($"//details?id={encoded}");Verify the value in
QueryPropertyon the target page.
Practical Verification
- Android:
adb shell am start -W -a android.intent.action.VIEW -d "https://example.com/items/42" com.company.appand watchadb logcatforShell.Navigatingevents. - iOS: Tap
https://example.com/items/42in Safari while the app is in the background; open Xcode Console to seeUIApplicationDelegatelogs. - Inspect
Shell.Current.CurrentState.Location.OriginalStringafter navigation to confirm the resolved URI matches the intent. - Check that
Application.Current.MainPagereferences a singleAppShellinstance by placing a breakpoint in its constructor.
Escalation Path
If all the above checks pass yet the deep link still fails, capture the following:
- Minimal repro project (AppShell, two pages, route registration).
- OS version, Xamarin.Forms version, and linker settings.
- Logs from
adb logcator Xcode Console showing the intent handling.
Open a GitHub issue in the Xamarin.Forms repository with these artifacts. The core team can then reproduce the issue in a controlled environment.
When to Migrate
Xamarin.Forms 5.0 is the last major release. If you’re starting a new project or can afford a rewrite, consider migrating to .NET MAUI Shell, which offers improved deep‑linking support and a unified API across Android, iOS, macOS, and Windows.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.