路由与导航:Navigator 与 go_router
Flutter 的导航分两层:底层的 Navigator(命令式压栈出栈)和上层路由方案(声明式、可用 URL 表达)。小应用用 Navigator.push(MaterialPageRoute(...)) 完全够;一旦需要深链接、Web URL、嵌套 Tab 各自独立的返回栈,就该上 go_router。
命令式导航:最直接的用法
// 跳转并等待返回值
final result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const DetailPage(id: 42)),
);
if (result != null) debugPrint('返回了:$result');
Navigator.pop(context, '已保存'); // 带结果返回
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const HomePage()));
Navigator.popUntil(context, (route) => route.isFirst); // 回到栈底
| 方法 | 行为 | 典型场景 |
|---|---|---|
push | 压入新页面 | 详情页 |
pop | 弹出当前页并回传结果 | 返回 |
pushReplacement | 替换当前页 | 登录 → 首页 |
popUntil | 连续弹出直到条件满足 | 回到首页 |
传参优先用构造函数:类型安全、可读性好;ModalRoute.of(context)!.settings.arguments 的类型是 Object?,只在路由表统一分发时才用。
命名路由与 onGenerateRoute
命名路由把"页面名 → 页面"集中到一处,适合页面多、需要统一入口的项目,但参数只能靠 arguments 传递,也没有深链接能力。
MaterialApp(
initialRoute: '/',
routes: {'/': (context) => const HomePage(), '/settings': (context) => const SettingsPage()},
onGenerateRoute: (settings) => MaterialPageRoute(
builder: (_) => settings.name == '/detail'
? DetailPage(id: settings.arguments as int? ?? 0)
: const Scaffold(body: Center(child: Text('页面不存在'))),
),
)
go_router:声明式路由
flutter pub add go_router
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final router = GoRouter(
initialLocation: '/',
// 全局重定向:未登录一律跳登录页;返回 null 表示不重定向
redirect: (context, state) {
const loggedIn = true; // 实际从状态管理里读
final toLogin = state.matchedLocation == '/login';
return !loggedIn && !toLogin ? '/login' : (loggedIn && toLogin ? '/' : null);
},
routes: [
GoRoute(path: '/', builder: (context, state) => const HomePage()),
GoRoute(path: '/login', builder: (context, state) => const LoginPage()),
// 路径参数用 :id 声明,查询参数从 uri.queryParameters 取
GoRoute(
path: '/detail/:id',
builder: (context, state) => DetailPage(
id: int.tryParse(state.pathParameters['id']!) ?? 0,
tab: state.uri.queryParameters['tab'] ?? 'info',
),
),
// ShellRoute:子页面共享底部导航栏,各自保留返回栈
ShellRoute(
builder: (context, state, child) => Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: state.uri.path.startsWith('/cart') ? 1 : 0,
onTap: (i) => context.go(i == 0 ? '/' : '/cart'),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.shopping_cart), label: '购物车'),
])),
routes: [GoRoute(path: '/cart', builder: (context, state) => const CartPage())],
),
],
// 兜底 404
errorBuilder: (context, state) => Scaffold(
appBar: AppBar(title: const Text('页面不存在')),
body: Center(child: Text('未找到:${state.uri}')),
),
);
void main() => runApp(MaterialApp.router(routerConfig: router));
context.go vs context.push
| 方式 | 行为 | 浏览器表现 | 适用场景 |
|---|---|---|---|
context.go('/detail/1') | 替换当前栈 | URL 直接替换 | Tab 切换、Web 主路径导航 |
context.push('/detail/1') | 压入新页面,保留返回栈 | 新增历史记录 | 详情页等需要返回的场景 |
context.pop() | 弹出一层 | 回退历史 | 返回 |
选错的最典型症状:用 go 进详情页后没有返回按钮,浏览器后退直接回首页。列表进详情用 push,底部导航切换用 go。
onTap: () => context.push('/detail/${item.id}?tab=spec'); // 列表进详情
onTap: (i) => context.go(i == 0 ? '/' : '/cart'); // 底部导航切换
多端与返回键处理
| 平台 | 要处理的事 | 做法 |
|---|---|---|
| Web | URL 与路由同步、刷新后停在原页 | 用路径式 go_router,避免只有名称无路径 |
| Android | 系统返回键 | go_router 自动处理,需二次确认时用 PopScope |
| iOS | 侧滑返回 | MaterialPageRoute 默认支持,fullscreenDialog 会关掉 |
| 桌面 | 窗口尺寸变化 | 用 LayoutBuilder 做响应式分栏 |
PopScope(
canPop: false, // 自己接管返回键,用于"未保存"二次确认
onPopInvokedWithResult: (didPop, result) async => didPop ? null : _confirmLeaveAndPop(),
child: const Scaffold(body: Center(child: Text('编辑页'))),
)
常见坑
| 现象 | 原因 | 修法 |
|---|---|---|
Navigator.of() called with a context that does not contain a Navigator | 用了 MaterialApp 之外的 context | 用页面的 context,确认没漏 MaterialApp |
push 之后 setState 报错 | 页面已销毁 | 加 if (!mounted) return; |
用 go 跳转后返回键失效 | go 替换了栈 | 改用 push |
| 深链接进不去 | 未配置平台 intent / universal link | Android 加 intent-filter,iOS 配 associated domains |
flutter run -d chrome # 在浏览器验证 URL 与后退行为
小结:简单应用用 Navigator.push(MaterialPageRoute(...)) 配构造函数传参、pop 回传结果即可;需要深链接、Web URL、嵌套栈时用 go_router——push 用于要返回的页面、go 用于替换式导航,路径参数与查询参数从 state 读,全局用 redirect 鉴权、errorBuilder 兜 404。