| 1 | --- |
| 2 | name: flutter-setup-declarative-routing |
| 3 | description: Configure `MaterialApp.router` using a package like `go_router` for advanced URL-based navigation. Use when developing web applications or mobile apps that require specific deep linking and browser history support. |
| 4 | metadata: |
| 5 | model: models/gemini-3.1-pro-preview |
| 6 | last_modified: Tue, 21 Apr 2026 21:08:03 GMT |
| 7 | --- |
| 8 | # Implementing Routing and Deep Linking |
| 9 | |
| 10 | ## Contents |
| 11 | - [Core Concepts](#core-concepts) |
| 12 | - [Workflow: Initializing the Application and Router](#workflow-initializing-the-application-and-router) |
| 13 | - [Workflow: Configuring Platform Deep Linking](#workflow-configuring-platform-deep-linking) |
| 14 | - [Workflow: Implementing Nested Navigation](#workflow-implementing-nested-navigation) |
| 15 | - [Examples](#examples) |
| 16 | |
| 17 | ## Core Concepts |
| 18 | |
| 19 | Use the `go_router` package for declarative routing in Flutter. It provides a robust API for complex routing scenarios, deep linking, and nested navigation. |
| 20 | |
| 21 | - **GoRouter**: The central configuration object defining the application's route tree. |
| 22 | - **GoRoute**: A standard route mapping a URL path to a Flutter screen. |
| 23 | - **ShellRoute / StatefulShellRoute**: Wraps child routes in a persistent UI shell (e.g., a `BottomNavigationBar`). `StatefulShellRoute` maintains the state of parallel navigation branches. |
| 24 | - **Path URL Strategy**: Removes the default `#` fragment from web URLs, essential for clean deep linking across platforms. |
| 25 | |
| 26 | ## Workflow: Initializing the Application and Router |
| 27 | |
| 28 | Follow this workflow to bootstrap a new Flutter application with `go_router` and configure the root routing mechanism. |
| 29 | |
| 30 | ### Task Progress |
| 31 | - [ ] Create the Flutter application. |
| 32 | - [ ] Add the `go_router` dependency. |
| 33 | - [ ] Configure the URL strategy for web/deep linking. |
| 34 | - [ ] Implement the `GoRouter` configuration. |
| 35 | - [ ] Bind the router to `MaterialApp.router`. |
| 36 | |
| 37 | ### 1. Scaffold the Application |
| 38 | Run the following commands to create the app and add the required routing package: |
| 39 | ```bash |
| 40 | flutter create <app-name> |
| 41 | cd <app-name> |
| 42 | flutter pub add go_router |
| 43 | ``` |
| 44 | |
| 45 | ### 2. Configure the Router |
| 46 | Define a top-level `GoRouter` instance. Handle authentication or state-based routing using the `redirect` parameter. |
| 47 | |
| 48 | ```dart |
| 49 | import 'package:flutter/material.dart'; |
| 50 | import 'package:go_router/go_router.dart'; |
| 51 | import 'package:flutter_web_plugins/url_strategy.dart'; |
| 52 | |
| 53 | void main() { |
| 54 | // Use path URL strategy to remove the '#' from web URLs |
| 55 | usePathUrlStrategy(); |
| 56 | runApp(const MyApp()); |
| 57 | } |
| 58 | |
| 59 | final GoRouter _router = GoRouter( |
| 60 | initialLocation: '/', |
| 61 | routes: [ |
| 62 | GoRoute( |
| 63 | path: '/', |
| 64 | builder: (context, state) => const HomeScreen(), |
| 65 | routes: [ |
| 66 | GoRoute( |
| 67 | path: 'details/:id', |
| 68 | builder: (context, state) => DetailsScreen(id: state.pathParameters['id']!), |
| 69 | ), |
| 70 | ], |
| 71 | ), |
| 72 | ], |
| 73 | errorBuilder: (context, state) => ErrorScreen(error: state.error), |
| 74 | ); |
| 75 | |
| 76 | class MyApp extends StatelessWidget { |
| 77 | const MyApp({super.key}); |
| 78 | |
| 79 | @override |
| 80 | Widget build(BuildContext context) { |
| 81 | return MaterialApp.router( |
| 82 | routerConfig: _router, |
| 83 | title: 'Routing App', |
| 84 | ); |
| 85 | } |
| 86 | } |
| 87 | ``` |
| 88 | |
| 89 | ## Workflow: Configuring Platform Deep Linking |
| 90 | |
| 91 | Configure the native platforms to intercept specific URLs and route them into the Flutter application. |
| 92 | |
| 93 | ### Task Progress |
| 94 | - [ ] Determine target platforms (iOS, Android, or both). |
| 95 | - [ ] Apply conditional configuration for Android (Manifest + Asset Links). |
| 96 | - [ ] Apply conditional configuration for iOS (Plist + Entitlements + AASA). |
| 97 | - [ ] Run validator -> review errors -> fix. |
| 98 | |
| 99 | ### If configuring for Android: |
| 100 | 1. **Modify `AndroidManifest.xml`**: Add the intent filter inside the `<activity>` tag for `.MainActivity`. |
| 101 | ```xml |
| 102 | <intent-filter android:autoVerify="true"> |
| 103 | <action android:name="android.intent.action.VIEW" /> |
| 104 | <category android:name="android.intent.category.DEFAULT" /> |
| 105 | <category android:name="android.intent.category.BROWSABLE" /> |
| 106 | <data android:scheme="http" android:host="yourdomain.com" /> |
| 107 | <data android:scheme="https" /> |
| 108 | </intent-filter> |
| 109 | ``` |
| 110 | 2. **Host `assetlinks.json`**: Serve the following JSON at `https://yourdomain.com/.well-known/assetlinks.json`. |
| 111 | ```json |
| 112 | [{ |
| 113 | "relation": ["delegate_permission/common.handle_all_urls"], |
| 114 | "target": { |
| 115 | "namespace": "android_app", |
| 116 | "package_name": "com.yourcompany.yourapp", |
| 117 | "sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"] |
| 118 | } |
| 119 | }] |
| 120 | ``` |
| 121 | |
| 122 | ### If configuring for iOS: |
| 123 | 1. **Modify `Info.plist`**: Opt-in to Flutter's default deep link handler. |
| 124 | *Note: If using a third-party deep linking plugin (e.g., `app_links`), set this to `NO` to prevent conflicts.* |
| 125 | ```xml |
| 126 | <key>FlutterDeepLinkingEnabled</key> |
| 127 | <true/> |
| 128 | ``` |
| 129 | 2. **Modify `Runner.entitlements`**: Add the associated domain. |
| 130 | ```xml |
| 131 | <key>com.apple.developer.associated-domains</key> |
| 132 | <array> |
| 133 | <string>applinks:yourdomain.com</string> |
| 134 | </array> |
| 135 | ``` |
| 136 | 3. **Host `apple-app-site-association`**: Serve the following JSON (without a `.json` extension) at `https://yourdomain.com/.well-known/apple-app-site-association`. |
| 137 | ```json |
| 138 | { |
| 139 | "applinks": { |
| 140 | "apps": [], |
| 141 | "details": [{ |
| 142 | "appIDs": ["TEAM_ID.com.yourcompany.yourapp"], |
| 143 | "paths": ["*"], |
| 144 | "components": [{"/": "/*"}] |
| 145 | }] |
| 146 | } |
| 147 | } |
| 148 | ``` |
| 149 | |
| 150 | ### Validation Loop |
| 151 | Run validator -> review errors -> fix. |
| 152 | - **Android**: Test using ADB. |
| 153 | ```bash |
| 154 | adb shell 'am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://yourdomain.com/details/123"' com.yourcompany.yourapp |
| 155 | ``` |
| 156 | - **iOS**: Test using `xcrun` on a booted simulator. |
| 157 | ```bash |
| 158 | xcrun simctl openurl booted https://yourdomain.com/details/123 |
| 159 | ``` |
| 160 | |
| 161 | ## Workflow: Implementing Nested Navigation |
| 162 | |
| 163 | Use `StatefulShellRoute` to implement persistent UI shells (like a bottom navigation bar) that maintain the state of their child routes. |
| 164 | |
| 165 | ### Task Progress |
| 166 | - [ ] Define `StatefulShellRoute.indexedStack` in the `GoRouter` configuration. |
| 167 | - [ ] Create `StatefulShellBranch` instances for each navigation tab. |
| 168 | - [ ] Implement the shell widget using `StatefulNavigationShell`. |
| 169 | |
| 170 | ```dart |
| 171 | final GoRouter _router = GoRouter( |
| 172 | initialLocation: '/home', |
| 173 | routes: [ |
| 174 | StatefulShellRoute.indexedStack( |
| 175 | builder: (context, state, navigationShell) { |
| 176 | return ScaffoldWithNavBar(navigationShell: navigationShell); |
| 177 | }, |
| 178 | branches: [ |
| 179 | StatefulShellBranch( |
| 180 | routes: [ |
| 181 | GoRoute( |
| 182 | path: '/home', |
| 183 | builder: (context, state) => const HomeScreen(), |
| 184 | ), |
| 185 | ], |
| 186 | ), |
| 187 | StatefulShellBranch( |
| 188 | routes: [ |
| 189 | GoRoute( |
| 190 | path: '/settings', |
| 191 | builder: (context, state) => const SettingsScreen(), |
| 192 | ), |
| 193 | ], |
| 194 | ), |
| 195 | ], |
| 196 | ), |
| 197 | ], |
| 198 | ); |
| 199 | ``` |
| 200 | |
| 201 | ## Examples |
| 202 | |
| 203 | ### High-Fidelity Shell Widget Implementation |
| 204 | Implement the UI shell that consumes the `StatefulNavigationShell` to handle branch switching. |
| 205 | |
| 206 | ```dart |
| 207 | class ScaffoldWithNavBar extends StatelessWidget { |
| 208 | const ScaffoldWithNavBar({ |
| 209 | required this.navigationShell, |
| 210 | super.key, |
| 211 | }); |
| 212 | |
| 213 | final StatefulNavigationShell navigationShell; |
| 214 | |
| 215 | void _goBranch(int index) { |
| 216 | navigationShell.goBranch( |
| 217 | index, |
| 218 | // Support navigating to the initial location when tapping the active tab. |
| 219 | initialLocation: index == navigationShell.currentIndex, |
| 220 | ); |
| 221 | } |
| 222 | |
| 223 | @override |
| 224 | Widget build(BuildContext context) { |
| 225 | return Scaffold( |
| 226 | body: navigationShell, |
| 227 | bottomNavigationBar: NavigationBar( |
| 228 | selectedIndex: navigationShell.currentIndex, |
| 229 | onDestinationSelected: _goBranch, |
| 230 | destinations: const [ |
| 231 | NavigationDestination(icon: Icon(Icons.home), label: 'Home'), |
| 232 | NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'), |
| 233 | ], |
| 234 | ), |
| 235 | ); |
| 236 | } |
| 237 | } |
| 238 | ``` |
| 239 | |
| 240 | ### Programmatic Navigation |
| 241 | Use the `context.go()` and `context.push()` extension methods provided by `go_router`. |
| 242 | |
| 243 | ```dart |
| 244 | // Replaces the current route stack with the target route (Declarative) |
| 245 | context.go('/details/123'); |
| 246 | |
| 247 | // Pushes the target route onto the existing stack (Imperative) |
| 248 | context.push('/details/123'); |
| 249 | |
| 250 | // Navigates using a named route and path parameters |
| 251 | context.goNamed('details', pathParameters: {'id': '123'}); |
| 252 | |
| 253 | // Pops the current route |
| 254 | context.pop(); |
| 255 | ``` |