GoRouter 完全指南

go_router 是 Flutter 官方推荐的声明式路由库,基于 Navigator 2.0,支持深链接、Web URL 同步、嵌套路由。 ShellRoute 为子路由提供共享的外壳 Widget(通常是带 BottomNavigationBar 的 Scaffold)。切换 Tab 时子路由各自维护独立的 Navigator,但 Shell Widget 本身不会重建。 StatefulShellRoute 在 ShellRoute 基础上为每个分支维护独立的 Navigator 状态,切换 Tab 时不会销毁页面。 refreshListena

分享

官方文档:https://pub.dev/packages/go_router
适用版本:go_router 14.x(2026-05-07 核实)

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

安装

# 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> 子路由列表

基础示例

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) 替换堆栈顶部 替换当前页,保留下层

命名路由导航

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

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

路径参数和查询参数读取

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);
  },
)

返回值传递

// 接收返回值的页面
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 本身不会重建。

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 状态

StatefulShellRouteShellRoute 基础上为每个分支维护独立的 Navigator 状态,切换 Tab 时不会销毁页面。

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(认证守卫)

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

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 集成

监听认证状态自动重定向

// 将 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,否则用户会积累大量历史记录,返回行为异常。

// 错误: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 定义完全一致

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

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

extra 参数不支持深链接

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

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

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

最佳实践

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

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

// 定义
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 回调:登录检查、权限验证放在顶层 GoRouterredirect 中,而非每个页面的 build 方法,保持路由层与 UI 层解耦。


常见陷阱

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

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

陷阱:context.gocontext.push 语义混淆

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

陷阱:redirect 中异步操作导致无限重定向

现象: redirect 回调中读取异步状态时,每次都返回重定向地址,导致页面反复跳转。
原因: redirect 是同步回调,使用 ref.watch 订阅异步 Provider 时 loading 状态触发了不期望的重定向。
解决: 使用 .whenDataAsyncValue.guard,loading 状态时返回 null(不重定向):

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

参见

Flutter入门
Flutter状态管理

阅读更多

Web 安全基础

1. HTML 转义(服务端渲染必须): 2. CSP(Content Security Policy): 3. HttpOnly Cookie:防止 JS 读取会话 Cookie: 4. 前端框架防护: 攻击者在第三方网站构造一个表单,诱导已登录用户提交,浏览器会自动携带目标站的 Cookie。 触发条件: 1. 用户已登录目标网站(Cookie 有效) 2. 目标 API 仅凭 Cookie 识别用户身份 3. 请求来源未验证 1. CSRF Token(推荐): 2. SameSite Cookie: 3. 验证 Origin/Referer 头:

By yellowdog

HTTP 协议深度指南

HTTP(HyperText Transfer Protocol)是 Web 的基础传输协议,基于 TCP/IP,采用请求/响应模型。 相关文档:Web安全基础(/web-an-quan-ji-chu/) FastAPI完全指南(/fastapi-wan-quan-zhi-nan/) Nginx完全指南(/nginx-wan-quan-zhi-nan/) 幂等性:多次执行相同请求,服务器状态结果相同。PUT /users/1 多次执行结果一致;POST /users 每次创建新资源,非幂等。 浏览器直接从本地缓存读取,不向服务器发送请求。 缓存命中时,状

By yellowdog

系统设计基础

SLA 对照表: 选择建议:无状态服务(Web 层、API 层)优先水平扩展;数据库初期垂直扩展,达到瓶颈后考虑分库分表或读写分离。 缓存穿透(查询不存在的 key,每次都打到 DB): 缓存击穿(热点 key 过期,瞬间大量请求打到 DB): 缓存雪崩(大量 key 同时过期,或缓存服务宕机): 令牌桶 Python 实现: Redis 实现分布式限流(滑动窗口): URL 命名规则: Cursor 分页响应格式: 雪花算法结构(64 bit): 定义:分布式系统不能同时满足以下三个特性: 在分布式环境中 P 是必须保证的,所以实际是 CP vs AP

By yellowdog

算法思路与模板

二分查找要求序列有序,每次将搜索范围缩减一半,时间复杂度 O(log n)。 两个指针从两端向中间收缩,常用于有序数组。 滑动窗口维护一个满足条件的区间 left, right,right 不断向右扩张,条件不满足时收缩 left。 滑动窗口通用框架: 1. 确定"子问题":原问题可以分解为哪些规模更小的同类问题 2. 定义 dpi 或 dpij 的含义,要足够清晰 3. 推导状态转移方程 4. 确定初始状态(边界条件) 5. 确定计算顺序(确保依赖的子问题先计算) 每件物品最多选一次。dpj = 容量为 j 时的最大价值,逆序遍历容量防止重复选取。 每

By yellowdog