> ## Content Index
> Fetch the complete content index at: https://blog.vercanti.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# GoRouter 完全指南
- URL: https://blog.vercanti.com/gorouter-wan-quan-zhi-nan/
- Published: 2026-08-28T14:35:44.000Z
- Updated: 2026-08-28T14:59:30.000Z
- Description: go_router 是 Flutter 官方推荐的声明式路由库，基于 Navigator 2.0，支持深链接、Web URL 同步、嵌套路由。 ShellRoute 为子路由提供共享的外壳 Widget（通常是带 BottomNavigationBar 的 Scaffold）。切换 Tab 时子路由各自维护独立的 Navigator，但 Shell Widget 本身不会重建。 StatefulShellRoute 在 ShellRoute 基础上为每个分支维护独立的 Navigator 状态，切换 Tab 时不会销毁页面。 refreshListena
- Author: yellowdog
- Tags: 移动开发

> 官方文档：<https://pub.dev/packages/go%5Frouter>  
> 适用版本：go\_router 14.x（2026-05-07 核实）

go\_router 是 Flutter 官方推荐的声明式路由库，基于 Navigator 2.0，支持深链接、Web URL 同步、嵌套路由。

## 安装

```yaml
# pubspec.yaml
dependencies:
  go_router: ^14.2.0

```

---

## 基础配置

### GoRouter 参数

| 参数                  | 类型                                                       | 说明                      |
| ------------------- | -------------------------------------------------------- | ----------------------- |
| routes              | List<RouteBase>                                          | 路由列表，必填                 |
| initialLocation     | String                                                   | 初始路径，默认 /               |
| redirect            | FutureOr<String?> Function(BuildContext, GoRouterState)? | 全局重定向，返回 null 表示不重定向    |
| errorBuilder        | Widget Function(BuildContext, GoRouterState)?            | 404 页面构建器               |
| navigatorKey        | GlobalKey<NavigatorState>?                               | 自定义 Navigator key       |
| debugLogDiagnostics | bool                                                     | 打印路由跳转日志，默认 false       |
| refreshListenable   | Listenable?                                              | 状态变化时触发全局 redirect 重新执行 |
| redirect 调用时机       | \-                                                       | 每次导航前执行                 |

### GoRoute 参数

| 参数          | 类型                                                       | 说明                  |
| ----------- | -------------------------------------------------------- | ------------------- |
| path        | String                                                   | 路径，支持 :param 占位符，必填 |
| name        | String?                                                  | 命名路由，用于 goNamed     |
| builder     | Widget Function(BuildContext, GoRouterState)?            | 构建 Widget           |
| pageBuilder | Page Function(BuildContext, GoRouterState)?              | 自定义 Page（控制转场动画）    |
| redirect    | FutureOr<String?> Function(BuildContext, GoRouterState)? | 路由级重定向              |
| routes      | List<RouteBase>                                          | 子路由列表               |

### 基础示例

```dart
import 'package:go_router/go_router.dart';

final router = GoRouter(
  initialLocation: '/',
  debugLogDiagnostics: true,
  routes: [
    GoRoute(
      path: '/',
      name: 'home',
      builder: (context, state) => const HomeScreen(),
      routes: [
        GoRoute(
          path: 'detail/:id',
          name: 'detail',
          builder: (context, state) {
            final id = state.pathParameters['id']!;
            return DetailScreen(id: id);
          },
        ),
      ],
    ),
    GoRoute(
      path: '/settings',
      name: 'settings',
      builder: (context, state) => const SettingsScreen(),
    ),
  ],
  errorBuilder: (context, state) => const NotFoundScreen(),
);

// 集成到 MaterialApp
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: router,
    );
  }
}

```

---

## 导航

### go / push / pop 区别

| 方法                            | 堆栈行为     | 适用场景          |
| ----------------------------- | -------- | ------------- |
| context.go(path)              | 替换整个导航堆栈 | Tab 切换、登录后跳首页 |
| context.push(path)            | 压入堆栈顶部   | 进入详情页，可以返回    |
| context.pop()                 | 弹出当前页面   | 返回上一页         |
| context.replace(path)         | 替换当前页面   | 不需要返回的跳转      |
| context.pushReplacement(path) | 替换堆栈顶部   | 替换当前页，保留下层    |

### 命名路由导航

```dart
// 路径参数 + 查询参数
context.goNamed(
  'detail',
  pathParameters: {'id': '123'},
  queryParameters: {'tab': 'comments'},
);

// 等价于跳转到 /detail/123?tab=comments

```

### 路径参数和查询参数读取

```dart
GoRoute(
  path: '/article/:id',
  builder: (context, state) {
    // 路径参数
    final id = state.pathParameters['id']!;
    // 查询参数
    final tab = state.uri.queryParameters['tab'] ?? 'content';
    return ArticleScreen(id: id, tab: tab);
  },
)

```

### 返回值传递

```dart
// 接收返回值的页面
Future<void> openEditPage() async {
  final result = await context.push<bool>('/edit');
  if (result == true) {
    // 刷新数据
    refreshData();
  }
}

// 返回值的页面
ElevatedButton(
  onPressed: () => context.pop(true), // 传递返回值
  child: const Text('保存'),
)

```

---

## 嵌套路由

### ShellRoute：共享底部导航栏

`ShellRoute` 为子路由提供共享的外壳 Widget（通常是带 `BottomNavigationBar` 的 Scaffold）。切换 Tab 时子路由各自维护独立的 Navigator，但 Shell Widget 本身不会重建。

```dart
final router = GoRouter(
  routes: [
    ShellRoute(
      builder: (context, state, child) => ScaffoldWithNavBar(child: child),
      routes: [
        GoRoute(
          path: '/home',
          builder: (context, state) => const HomeScreen(),
        ),
        GoRoute(
          path: '/profile',
          builder: (context, state) => const ProfileScreen(),
        ),
      ],
    ),
  ],
);

class ScaffoldWithNavBar extends StatelessWidget {
  final Widget child;
  const ScaffoldWithNavBar({super.key, required this.child});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: child,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: _calculateIndex(context),
        onTap: (index) {
          switch (index) {
            case 0:
              context.go('/home');
            case 1:
              context.go('/profile');
          }
        },
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }

  int _calculateIndex(BuildContext context) {
    final location = GoRouterState.of(context).uri.toString();
    if (location.startsWith('/profile')) return 1;
    return 0;
  }
}

```

### StatefulShellRoute：保持各 Tab 状态

`StatefulShellRoute` 在 `ShellRoute` 基础上为每个分支维护独立的 Navigator 状态，切换 Tab 时不会销毁页面。

```dart
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: '/profile',
          builder: (context, state) => const ProfileScreen(),
        ),
      ],
    ),
  ],
)

// 使用 navigationShell 切换 Tab
class ScaffoldWithNavBar extends StatelessWidget {
  final StatefulNavigationShell navigationShell;
  const ScaffoldWithNavBar({super.key, required this.navigationShell});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: BottomNavigationBar(
        currentIndex: navigationShell.currentIndex,
        onTap: (index) => navigationShell.goBranch(
          index,
          initialLocation: index == navigationShell.currentIndex,
        ),
        items: const [
          BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
          BottomNavigationBarItem(icon: Icon(Icons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

```

---

## 重定向与守卫

### 全局 redirect（认证守卫）

```dart
final router = GoRouter(
  redirect: (context, state) {
    final isLoggedIn = AuthService.instance.isLoggedIn;
    final isLoginPage = state.matchedLocation == '/login';

    if (!isLoggedIn && !isLoginPage) {
      // 未登录跳转到登录页，并记录原始路径
      return '/login?redirect=${state.matchedLocation}';
    }
    if (isLoggedIn && isLoginPage) {
      // 已登录不需要再访问登录页
      return '/home';
    }
    return null; // 不重定向
  },
  routes: [...],
);

```

### 路由级 redirect

```dart
GoRoute(
  path: '/admin',
  redirect: (context, state) {
    if (!AuthService.instance.isAdmin) {
      return '/home';
    }
    return null;
  },
  builder: (context, state) => const AdminScreen(),
)

```

### GoRouterState 字段

| 字段              | 类型                  | 说明               |
| --------------- | ------------------- | ---------------- |
| uri             | Uri                 | 完整 URI，包含路径、查询参数 |
| matchedLocation | String              | 当前匹配的路径（不含查询参数）  |
| pathParameters  | Map<String, String> | 路径参数（:id 对应的值）   |
| extra           | Object?             | 通过 extra 参数传递的对象 |
| error           | Exception?          | 路由错误信息           |
| pageKey         | ValueKey<String>    | 当前 Page 的唯一 key  |

---

## 与 Riverpod 集成

### 监听认证状态自动重定向

```dart
// 将 Riverpod Provider 转为 Listenable
class ProviderListenable<T> extends ChangeNotifier {
  ProviderListenable(this._ref, this._provider) {
    _ref.listen(_provider, (_, __) => notifyListeners());
  }

  final Ref _ref;
  final ProviderListenable<T> _provider;
}

// 推荐：使用 riverpod_router 或自定义 refreshListenable
final authNotifierProvider = ChangeNotifierProvider<AuthNotifier>((ref) {
  return AuthNotifier();
});

class AuthNotifier extends ChangeNotifier {
  bool _isLoggedIn = false;
  bool get isLoggedIn => _isLoggedIn;

  void login() {
    _isLoggedIn = true;
    notifyListeners();
  }

  void logout() {
    _isLoggedIn = false;
    notifyListeners();
  }
}

// 在 GoRouter 中使用 refreshListenable
GoRouter buildRouter(WidgetRef ref) {
  final authNotifier = ref.watch(authNotifierProvider);
  return GoRouter(
    refreshListenable: authNotifier,
    redirect: (context, state) {
      if (!authNotifier.isLoggedIn && state.matchedLocation != '/login') {
        return '/login';
      }
      return null;
    },
    routes: [...],
  );
}

```

### refreshListenable 说明

`refreshListenable` 接收一个 `Listenable`，当该对象发出通知时，GoRouter 会重新执行全局 `redirect` 函数，从而在认证状态变化时自动触发路由更新。

---

## 踩坑与注意事项

### context.go 与 context.push 的堆栈差异

`context.go` 会清空现有导航堆栈并跳转，`context.push` 则保留堆栈。在 Tab 页面间切换时应使用 `context.go`，否则用户会积累大量历史记录，返回行为异常。

```dart
// 错误：Tab 切换用 push 会导致历史堆积
onTap: (index) => context.push('/profile'),

// 正确：Tab 切换用 go
onTap: (index) => context.go('/profile'),

```

### ShellRoute 中的 pop 行为

在 `ShellRoute` 的子路由中调用 `context.pop()` 只会弹出该分支内的页面。如果分支内没有更多页面可弹，`pop` 不会关闭 ShellRoute 本身，这通常是期望的行为，但要注意不要依赖 `context.canPop()` 来判断是否可以退出 Tab。

### 路径参数必须与 path 定义完全一致

```dart
// path 定义
GoRoute(path: '/user/:userId/post/:postId', ...)

// 读取时参数名必须完全匹配
final userId = state.pathParameters['userId']!;
final postId = state.pathParameters['postId']!;

```

### extra 参数不支持深链接

通过 `extra` 传递的对象在应用热重载或深链接恢复时会丢失，因为它无法序列化到 URL 中。需要持久化的数据应通过路径参数或查询参数传递。

```dart
// 不推荐：深链接场景下 extra 会丢失
context.push('/detail/123', extra: userObject);

// 推荐：通过 ID 传递，在目标页重新获取
context.push('/detail/123');

```

---

## 最佳实践

**所有路由集中在一个 `GoRouter` 实例中声明**：避免分散定义路由，集中管理便于全局守卫和深链接配置，通常放在 `router.dart` 单独文件中，通过 Riverpod/Provider 注入。

**用命名路由 `context.goNamed` 而非路径字符串**：路径字符串散落在代码中难以重构，命名路由支持类型安全的 `pathParameters`：

```dart
// 定义
GoRoute(
  path: '/user/:id',
  name: 'user-detail',
  builder: (context, state) => UserDetail(id: state.pathParameters['id']!),
),

// 跳转
context.goNamed('user-detail', pathParameters: {'id': userId});

```

**复杂页面用 `ShellRoute` 实现嵌套导航**：底部导航栏场景用 `ShellRoute` 包裹，保留各 Tab 的导航栈状态，而非每次切换 Tab 都重建页面。

**深链接测试用 `adb` 命令验证**：Android 调试时用 `adb shell am start -a android.intent.action.VIEW -d "yourscheme://path"` 测试深链接跳转，不要只靠模拟器内点击。

**路由守卫统一写在 `redirect` 回调**：登录检查、权限验证放在顶层 `GoRouter` 的 `redirect` 中，而非每个页面的 `build` 方法，保持路由层与 UI 层解耦。

---

## 常见陷阱

### 陷阱：`extra` 参数在深链接和 Web 刷新后丢失

**现象：** 通过 `context.push('/detail', extra: user)` 传递对象，用户刷新页面后 `extra` 为 `null`。  
**原因：** `extra` 存在内存中，不序列化到 URL，深链接和 Web 刷新后无法恢复。  
**解决：** 通过 `pathParameters` 或 `queryParameters` 传递 ID，在目标页重新请求数据。

### 陷阱：`context.go` 与 `context.push` 语义混淆

**现象：** 使用 `context.go('/home')` 后，返回按钮无法返回上一页。  
**原因：** `go` 替换整个导航栈，`push` 在栈顶压入新页面。`go` 适合 Tab 切换，`push` 适合页面堆叠。  
**解决：** 导航到新页面用 `context.push`，重置到根页面（如登出后回首页）用 `context.go`。

### 陷阱：`redirect` 中异步操作导致无限重定向

**现象：** `redirect` 回调中读取异步状态时，每次都返回重定向地址，导致页面反复跳转。  
**原因：** `redirect` 是同步回调，使用 `ref.watch` 订阅异步 Provider 时 loading 状态触发了不期望的重定向。  
**解决：** 使用 `.whenData` 或 `AsyncValue.guard`，loading 状态时返回 `null`（不重定向）：

```dart
redirect: (context, state) {
  final authState = ref.read(authProvider);
  if (authState.isLoading) return null; // 加载中不做任何重定向
  if (!authState.isAuthenticated) return '/login';
  return null;
},

```

---

## 参见

[Flutter入门](https://blog.vercanti.com/flutter-ru-men/)  
[Flutter状态管理](https://blog.vercanti.com/flutter-zhuang-tai-guan-li/)