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 yourmain.dartfile 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 thestate.pathParametersmap 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 fromflutter_web_pluginsthat 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.
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
GoRouterinstance – Declare all routes, redirects, and error handlers in one immutable configuration object according to the flutter/skills guidelines. - Clean web URLs – Call
usePathUrlStrategy()beforerunApp()to remove hash fragments from web addresses. MaterialApp.routerbinding – Required to synchronize the router with Flutter's widget tree and system navigation events.- Platform manifests – Configure
AndroidManifest.xmlintent filters and iOSInfo.plistentries to enable external deep linking. - Nested state preservation – Use
StatefulShellRoutefor 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.
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 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →