go_router 路由
一句话理解
go_router 是 Flutter 官方推荐的路由方案——用声明式的方式写路由,天然支持 Web 和深度链接。
适合新项目直接使用,不必再学
Navigator的命令式写法。
核心概念
声明式路由 vs 命令式路由
| 命令式(Navigator) | 声明式(go_router) | |
|---|---|---|
| 思路 | 在代码中手动调用 push/pop | 提前定义路由表,框架自动处理 |
| 比喻 | 自己开车去每个目的地 | 提前规划好地图,导航仪带你走 |
| 代码风格 | Navigator.push(context, MaterialPageRoute(...)) | context.push('/detail') |
简单说:声明式就是先定义好"有哪些路径、每个路径对应什么页面",然后只管跳路径就行。
GoRoute
GoRoute 是路由表中的一条规则,包含 path(路径)和 builder(构建页面):
GoRoute(
path: '/detail', // 路径
builder: (context, state) => const DetailPage(), // 对应的页面
),GoRouterState
builder 的第二个参数 state 是 GoRouterState,携带了跳转时的所有信息:
| 属性 | 含义 |
|---|---|
state.pathParameters | 路径参数(如 /user/:id 中的 id) |
state.uri.queryParameters | 查询参数(如 ?keyword=flutter) |
state.extra | 额外传递的对象 |
state.matchedLocation | 当前匹配到的路径 |
MaterialApp.router
使用 go_router 时,必须用 MaterialApp.router 而不是普通的 MaterialApp:
// ❌ 普通方式(不支持 go_router)
MaterialApp(home: HomePage());
// ✅ go_router 方式
MaterialApp.router(routerConfig: router);.router 构造函数让 go_router 接管整个路由系统。
context 上的路由方法
context.push()、context.go()、context.pop() 并不是 BuildContext 自带的方法,而是 go_router 提供的扩展方法。只要 import 'package:go_router/go_router.dart',就能在任意 context 上调用。
深度链接
深度链接是指从外部直接打开应用的某个页面,比如:
- 在浏览器地址栏输入
https://yourapp.com/detail/42,应用直接打开商品详情页 - 点击一个推送通知,直接跳到对应页面
go_router 内置支持深度链接,只需路径对应上就能自动跳转。Navigator 要实现这个功能需要手写大量代码。
安装
flutter pub add go_router5 分钟上手
第一步:定义路由表
import 'package:go_router/go_router.dart';
final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/detail',
builder: (context, state) => const DetailPage(),
),
],
);第二步:挂到 MaterialApp
MaterialApp.router(
routerConfig: router,
)第三步:跳转和返回
// 跳转
context.go('/detail'); // 替换式跳转(类似 pushReplacement)
context.push('/detail'); // 压栈跳转(可以返回)⭐ 常用
// 返回
context.pop();新手提示:大多数情况用
context.push(),这样用户可以点返回键回到上一页。
路径参数(动态路由)
用 :变量名 定义路径参数,类似 Web 开发中的 /user/:id:
GoRoute(
path: '/user/:id', // :id 是路径参数
builder: (context, state) {
final id = state.pathParameters['id']!; // 取出参数
return UserPage(id: id);
},
),跳转方式:
context.push('/user/42'); // id 就是 '42'路径参数适合必须传的数据,如商品 ID、用户 ID。
查询参数
查询参数就是 URL 中 ?key=value 的部分,适合可选的数据:
// 跳转:/search?keyword=flutter&page=1
context.push('/search?keyword=flutter&page=1');
// 获取参数
GoRoute(
path: '/search',
builder: (context, state) {
final keyword = state.uri.queryParameters['keyword']; // 'flutter'
final page = int.tryParse(state.uri.queryParameters['page'] ?? '1'); // 1
return SearchPage(keyword: keyword, page: page);
},
),额外参数:extra
如果数据不是字符串(如自定义对象),用 extra 传递:
// 跳转时传
context.push('/detail', extra: Product(id: 42, name: '手机'));
// 接收
GoRoute(
path: '/detail',
builder: (context, state) {
final product = state.extra as Product; // 取出并转换类型
return DetailPage(product: product);
},
),
extra可以传任意类型的对象,比命名路由的arguments好用。
嵌套路由
嵌套路由用于共享布局,比如底部导航栏不变,只切换中间内容:
GoRoute(
path: '/user',
builder: (context, state) => const UserShell(), // 外壳(含底部导航)
routes: [
GoRoute(
path: 'profile', // 实际路径: /user/profile
builder: (context, state) => const ProfilePage(),
),
GoRoute(
path: 'settings', // 实际路径: /user/settings
builder: (context, state) => const SettingsPage(),
),
],
),错误处理
当用户访问一个不存在的路径时,go_router 会显示默认的错误页面。你也可以自定义:
GoRouter(
errorBuilder: (context, state) => const NotFoundPage(),
routes: [...],
)重定向(路由守卫)
重定向可以在跳转前做判断,最常用的场景是登录检查:
GoRouter(
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
final isLoginPage = state.matchedLocation == '/login';
// 未登录且不在登录页 → 跳转到登录页
if (!isLoggedIn && !isLoginPage) return '/login';
// 已登录且在登录页 → 跳转到首页
if (isLoggedIn && isLoginPage) return '/';
// 不需要重定向
return null;
},
routes: [...],
)go vs push 的区别
| 方法 | 行为 | 能否返回 | 适合场景 |
|---|---|---|---|
context.go('/detail') | 替换栈顶 | ❌ 不能 | 切换底部 Tab、登录后跳首页 |
context.push('/detail') | 压入栈顶 | ✅ 能 | 正常的页面跳转 |
简单记忆:需要能返回就用
push,不需要返回就用go。
go_router 与 Navigator 对比
| 对比项 | Navigator | go_router |
|---|---|---|
| 风格 | 命令式(手动 push/pop) | 声明式(定义路由表) |
| 路径参数 | 需手动解析 | 自动解析 :id |
| Web 浏览器支持 | 有限 | 完整 |
| 深度链接 | 手动处理 | 内置支持 |
| 路由守卫 | 无 | redirect |
| 传参方式 | 构造函数 / arguments | extra / 路径参数 / 查询参数 |
| 官方推荐 | ❌ 旧方案 | ✅ 推荐方案 |
完整示例
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() => runApp(const MyApp());
// ---- 定义路由 ----
final router = GoRouter(
redirect: (context, state) {
// 简单的登录检查
final loggedIn = false; // 换成你的登录状态
final isLogin = state.matchedLocation == '/login';
if (!loggedIn && !isLogin) return '/login';
return null;
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/detail/:id',
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailPage(id: id);
},
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
],
);
// ---- App 入口 ----
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
);
}
}
// ---- 首页 ----
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: ElevatedButton(
child: const Text('查看商品 42'),
onPressed: () => context.push('/detail/42'),
),
),
);
}
}
// ---- 详情页 ----
class DetailPage extends StatelessWidget {
final String id;
const DetailPage({super.key, required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('商品 $id')),
body: Center(
child: ElevatedButton(
child: const Text('返回'),
onPressed: () => context.pop(),
),
),
);
}
}
// ---- 登录页 ----
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('登录')),
body: const Center(child: Text('请登录')),
);
}
}小结
| 你想做什么 | 代码 |
|---|---|
| 跳转(可返回) | context.push('/detail') |
| 跳转(不可返回) | context.go('/home') |
| 返回上一页 | context.pop() |
| 传路径参数 | context.push('/user/42') → state.pathParameters['id'] |
| 传查询参数 | context.push('/search?q=flutter') → state.uri.queryParameters['q'] |
| 传对象 | context.push('/detail', extra: obj) → state.extra as Obj |
| 登录拦截 | redirect 回调 |
