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

> Master Flutter declarative routing and deep linking with GoRouter. Learn to configure routes, handle web URLs, and manage external links efficiently for a seamless app experience.

- Repository: [Flutter/skills](https://github.com/flutter/skills)
- Tags: how-to-guide
- Published: 2026-05-09

---

**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`](https://github.com/flutter/skills/blob/main/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`](https://github.com/flutter/skills/blob/main/skills/flutter-setup-declarative-routing/SKILL.md) demonstrates the standard setup:

```dart
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:

```dart
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`](https://github.com/flutter/skills/blob/main/android/app/src/main/AndroidManifest.xml) within your main activity declaration to handle HTTPS scheme links:

```xml
<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:

```xml
<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:

```dart
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.

## Navigation Patterns: Declarative vs Imperative

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`](https://github.com/flutter/skills/blob/main/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`](https://github.com/flutter/skills/blob/main/index.html) for all routes.

### How do I test deep links on Android during development?

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`](https://github.com/flutter/skills/blob/main/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.