Deep Linking with go_router

Medium PriorityAsked in ~60% of senior interviews

4 min read

Navigation

User taps myapp.com/products/123
   │
   ├── App not running? OS launches it WITH the URL → main() → router parses
   │
   └── App running?     OS routes URL to existing instance → router parses
                                                                ↓
                                                  ProductScreen(id: '123')

Three things must line up:

  1. OS-level claim: your app declares "I handle myapp.com / com.example://".
  2. Verification (iOS Universal Links / Android App Links): server-hosted apple-app-site-association / assetlinks.json files prove you own the domain.
  3. In-app routing: go_router converts the URL into a route stack.

Without #1/#2, the URL opens in the browser. Without #3, you get the launch screen but no navigation.


Code in action

final router = GoRouter(
  initialLocation: '/',
  debugLogDiagnostics: true,

  routes: [
    GoRoute(
      path: '/',
      builder: (ctx, st) => const HomeScreen(),
      routes: [
        GoRoute(
          name: 'product',
          path: 'products/:id',                          // path parameter
          builder: (ctx, st) => ProductScreen(
            productId: st.pathParameters['id']!,
          ),
        ),
        GoRoute(
          path: 'profile',
          builder: (ctx, st) => const ProfileScreen(),
          routes: [
            GoRoute(
              path: 'settings',
              builder: (ctx, st) => const SettingsScreen(),
            ),
          ],
        ),
      ],
    ),
  ],

  // Auth guard
  redirect: (ctx, st) {
    final loggedIn = AuthService.instance.isLoggedIn;
    final atLogin  = st.matchedLocation == '/login';
    if (!loggedIn && !atLogin) return '/login?from=${st.matchedLocation}';
    if (loggedIn && atLogin)   return '/';
    return null;
  },

  errorBuilder: (ctx, st) => const NotFoundScreen(),
);
// Navigation
context.go('/products/123');                              // replace stack
context.push('/products/123');                            // push on top
context.goNamed('product', pathParameters: {'id': '123'});

Platform setup (the part people forget)

Android — AndroidManifest.xml:

<activity android:name=".MainActivity" ...>
  <!-- App link (verified) -->
  <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>
</activity>
  • Host https://myapp.com/.well-known/assetlinks.json proving ownership.

iOS — Runner.entitlements:

<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:myapp.com</string>
</array>
  • Host https://myapp.com/.well-known/apple-app-site-association.

Web: go_router uses the URL directly — pushState integration is automatic.


Where to handle which

ConcernUse
Auth gatingredirect: at GoRouter root
Per-route guardsPer-route redirect:
404 / unknown URLerrorBuilder:
Loading state while resolving (e.g., wait for auth)A wrapper widget around the route's child
Push from notification when app is in foregroundcontext.push(notificationDeepLink)
Push from notification when app was killedgetInitialMessage() in your init flow → router.go(...) once app is ready
Branch.io / Adjust / Firebase Dynamic LinksUse the SDK to receive the URL, then forward to router.go()

Common mistakes to avoid

❌ Skipping platform configuration
   Code looks right; URL opens browser instead of app. The OS doesn't know
   your app claims that URL.

❌ Forgetting the .well-known files for Universal Links / App Links
   Without verification, iOS shows a "open in app?" prompt or just opens Safari.

❌ Auth redirect that returns the wrong destination after login
   User taps a deep link → redirected to /login → after login → home (lost their target).
   ✅ Preserve the original path: '/login?from=$matchedLocation', then redirect back.

❌ context.go for a flow that should be pushable
   go REPLACES the stack — back button takes you out of the app instead of back to home.
   ✅ Use push for nested flows; go for "go here" operations like logout.

❌ Trying to access ScopedDependencies before they're ready
   Deep link fires before Firebase / DI is initialized.
   ✅ Show a SplashRoute that awaits init, then redirects to the original path.

❌ Reusing path parameter names across nested routes
   /:id at two levels → both populate state.pathParameters['id'], you lose info.
   ✅ Use distinct names: '/orders/:orderId/items/:itemId'.

Interview follow-ups

  1. How do you preserve the user's intended destination through an auth redirect? Capture st.matchedLocation in the redirect, encode it as a query parameter (/login?from=...), and after successful login context.go(decoded). Standard "deep link survives login" pattern.

  2. How do you handle a deep link that arrives before the app is initialized? For terminated state, read FirebaseMessaging.getInitialMessage() (or getInitialLink() from uni_links/app_links) inside your app's init phase. Show a splash while you initialize, then router.go(deepLinkPath) once dependencies are ready.

  3. What's the difference between scheme-based and Universal/App Links? myapp://... is a custom URL scheme — works without verification but can be hijacked by any app claiming the same scheme. Universal Links (iOS) / App Links (Android) use HTTPS URLs and require server-hosted verification files — they're tied to your domain, so no other app can claim them.

  4. When would you NOT use go_router? For tiny apps with 2-3 imperative pushes, plain Navigator is fine. For very custom Router 2.0 setups (parallel navigators, exotic state restoration), the lower-level Router API may give more control. For the 90% case — auth flow, web, deep links, nested layouts — go_router is the right default.


How helpful was this content?

Please sign in to rate this article.