How to Configure GoRouter for Declarative Routing and Deep Linking in Flutter

Configure GoRouter by creating a single router instance that declares your entire route tree, calling usePathUrlStrategy() before runApp() for clean web URLs, binding it to MaterialApp.router, and adding platform-specific intent filters in AndroidManifest.xml and Info.plist to handle external deep links.

The go_router package is the recommended solution for declarative routing in Flutter, providing a type-safe, URL-based navigation system that synchronizes with the browser address bar and platform-specific manifest files. According to the flutter/skills repository, you implement this architecture by defining a central GoRouter configuration in lib/main.dart and supplementing it with native platform settings to enable deep linking on Android and iOS.

Core Architecture Components

The declarative routing system relies on four key components that separate routing logic from UI implementation:

  • GoRouter – A single central object declared in your main.dart file that stores the entire route tree, redirect logic, error handling, and navigation observers. This acts as the single source of truth for all navigation state.
  • GoRoute – Maps a URL path pattern (e.g., /details/:id) to a specific screen widget. Path parameters are accessed via the state.pathParameters map within the route builder.
  • ShellRoute / StatefulShellRoute – Wraps child routes in a persistent UI shell (such as a bottom navigation bar) while preserving the navigation state of each branch independently.
  • usePathUrlStrategy() – A function from flutter_web_plugins that removes the hash (#) fragment from web URLs, enabling clean deep links that resemble standard web addresses.

Step 1: Create the Router and Enable Path URL Strategy

Initialize the router at the top level of your application and enable the path-based URL strategy for web compatibility. The following implementation from skills/flutter-setup-declarative-routing/SKILL.md demonstrates the standard setup:

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_web_plugins/url_strategy.dart';

void main() {
  // Remove the '#' from web URLs for clean deep linking
  usePathUrlStrategy();
  
  runApp(const MyApp());
}

final GoRouter _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'details/:id',
          builder: (context, state) => DetailsScreen(
            id: state.pathParameters['id']!,
          ),
        ),
      ],
    ),
  ],
  errorBuilder: (context, state) => ErrorScreen(error: state.error),
);

Step 2: Bind GoRouter to MaterialApp.router

Replace your standard MaterialApp with MaterialApp.router and pass the router configuration via the routerConfig parameter. This binding automatically wires navigation callbacks, system back-button handling, and URL synchronization:

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
      title: 'Declarative Routing Demo',
    );
  }
}

Step 3: Configure Platform-Specific Deep Linking

For mobile deep linking to function, you must declare intent filters in the native platform configuration files so the OS knows to launch your app when users tap external URLs.

Android Configuration

Add an intent filter to android/app/src/main/AndroidManifest.xml within your main activity declaration to handle HTTPS scheme links:

<activity android:name=".MainActivity">
  <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="yourdomain.com" />
  </intent-filter>
</activity>

iOS Configuration

Enable deep linking in ios/Runner/Info.plist by adding the Flutter deep linking flag:

<key>FlutterDeepLinkingEnabled</key>
<true/>

For production apps, also configure Associated Domains in Runner.entitlements to support Universal Links.

Step 4: Implement Nested Navigation with StatefulShellRoute

When your app requires persistent UI elements (such as a bottom navigation bar) that remain visible across multiple destinations, use StatefulShellRoute to maintain independent navigation stacks for each tab:

final GoRouter _router = GoRouter(
  initialLocation: '/home',
  routes: [
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithNavBar(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/home',
              builder: (context, state) => const HomeScreen(),
            ),
          ],
        ),
        StatefulShellBranch(
          routes: [
            GoRoute(
              path: '/settings',
              builder: (context, state) => const SettingsScreen(),
            ),
          ],
        ),
      ],
    ),
  ],
);

This configuration creates two independent navigation branches where each tab maintains its own history stack and state.

While the router configuration is declarative, you can navigate programmatically using context extensions that manipulate the underlying navigation stack:

  • context.go('/details/123') – Declaratively replaces the entire navigation stack with the new route, equivalent to setting the URL directly.
  • context.push('/details/123') – Imperatively pushes the route onto the current stack, preserving the previous route for back navigation.
  • context.goNamed('details', pathParameters: {'id': '123'}) – Navigates using named routes with typed path parameters to minimize runtime errors.
  • context.pop() – Removes the current route from the stack and returns to the previous screen.

Summary

  • Single GoRouter instance – Declare all routes, redirects, and error handlers in one immutable configuration object according to the flutter/skills guidelines.
  • Clean web URLs – Call usePathUrlStrategy() before runApp() to remove hash fragments from web addresses.
  • MaterialApp.router binding – Required to synchronize the router with Flutter's widget tree and system navigation events.
  • Platform manifests – Configure AndroidManifest.xml intent filters and iOS Info.plist entries to enable external deep linking.
  • Nested state preservation – Use StatefulShellRoute for bottom navigation bars and other persistent UI shells that require independent branch navigation.

Frequently Asked Questions

What is the difference between ShellRoute and StatefulShellRoute in GoRouter?

ShellRoute provides a persistent UI wrapper around child routes but creates a new instance of the shell widget for each navigation change, while StatefulShellRoute maintains state across tab switches using an IndexedStack, making it ideal for bottom navigation bars where each tab must preserve its scroll position and navigation history independently.

How do I extract path parameters from a URL in GoRouter?

Path parameters defined in the route path (e.g., :id in /details/:id) are accessed via the state.pathParameters map within the builder callback, typed as Map<String, String>. Always validate or provide defaults for nullable values before casting, as shown in the pattern: id: state.pathParameters['id']!.

Why does my Flutter web app show a hash (#) in the URL when using GoRouter?

The hash appears because Flutter defaults to hash-based URL strategy for compatibility with single-page applications on static file servers. To remove it, import package:flutter_web_plugins/url_strategy.dart and call usePathUrlStrategy() before runApp(), which requires proper server configuration to fallback to index.html for all routes.

Use the Android Debug Bridge (ADB) to simulate incoming intents from the command line: adb shell 'am start -W -a android.intent.action.VIEW -d "https://yourdomain.com/details/123" com.example.yourapp'. Ensure your AndroidManifest.xml contains the appropriate intent-filter with android:autoVerify="true" and that the domain has the proper Digital Asset Links JSON file hosted for App Links verification.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →