导航与路由
导航是让用户在不同页面之间跳转的核心机制。Flutter 提供了两种导航方案:基础的 Navigator 和声明式的 go_router。
Navigator — 基础导航
基本跳转
dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 跳转到新页面
// ─── ★ Navigator.push ──────────────
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const DetailPage()),
);
// ─── ☆ Navigator.push ──────────────
},
child: const Text('跳转到详情页'),
),
),
);
}
}
class DetailPage extends StatelessWidget {
const DetailPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('详情页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 返回上一页
// ─── ★ Navigator.pop ──────────────
Navigator.of(context).pop();
// ─── ☆ Navigator.pop ──────────────
},
child: const Text('返回'),
),
),
);
}
}带参数跳转
dart
// 跳转时传参
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => DetailPage(id: 123, title: '详情'),
),
);
// 接收参数
class DetailPage extends StatelessWidget {
final int id;
final String title;
const DetailPage({super.key, required this.id, required this.title});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(title)),
body: Text('ID: $id'),
);
}
}接收返回值
dart
import 'package:flutter/material.dart';
class SelectCityPage extends StatelessWidget {
const SelectCityPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('选择城市')),
body: ListView(
children: [
ListTile(
title: const Text('北京'),
onTap: () => Navigator.of(context).pop('北京'),
),
ListTile(
title: const Text('上海'),
onTap: () => Navigator.of(context).pop('上海'),
),
ListTile(
title: const Text('广州'),
onTap: () => Navigator.of(context).pop('广州'),
),
],
),
);
}
}
class CityPickerPage extends StatefulWidget {
const CityPickerPage({super.key});
@override
State<CityPickerPage> createState() => _CityPickerPageState();
}
class _CityPickerPageState extends State<CityPickerPage> {
String _selectedCity = '未选择';
void _pickCity() async {
// 跳转并等待返回值
final result = await Navigator.of(context).push<String>(
MaterialPageRoute(builder: (context) => const SelectCityPage()),
);
if (result != null) {
setState(() => _selectedCity = result);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('选择城市')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('当前城市: $_selectedCity', style: const TextStyle(fontSize: 20)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _pickCity,
child: const Text('选择城市'),
),
],
),
),
);
}
}命名路由
在 MaterialApp 中定义路由表:
dart
MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => HomePage(),
'/detail': (context) => DetailPage(),
'/settings': (context) => SettingsPage(),
},
)使用命名路由跳转:
dart
import 'package:flutter/material.dart';
class NamedRouteHome extends StatelessWidget {
const NamedRouteHome({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed('/detail'),
child: const Text('跳转详情'),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.of(context).pushNamed(
'/detail',
arguments: {'id': 123},
),
child: const Text('带参数跳转'),
),
],
),
),
);
}
}
class NamedDetailPage extends StatelessWidget {
const NamedDetailPage({super.key});
@override
Widget build(BuildContext context) {
final args = ModalRoute.of(context)!.settings.arguments as Map?;
return Scaffold(
appBar: AppBar(title: const Text('详情页')),
body: Center(
child: Text('ID: ${args?['id'] ?? '无参数'}'),
),
);
}
}
### 替换与清空
```dart
import 'package:flutter/material.dart';
// 替换当前页面(比如登录后替换为首页)
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
void _login() {
// 模拟登录成功后替换为首页
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const HomePage()),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('登录')),
body: Center(
child: ElevatedButton(
onPressed: _login,
child: const Text('登录'),
),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('首页')),
body: Center(
child: ElevatedButton(
onPressed: () {
// 退出登录:清空所有页面并跳转登录页
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginPage()),
(route) => false,
);
},
child: const Text('退出登录'),
),
),
);
}
}go_router — 声明式路由(推荐)
go_router 是 Flutter 官方推荐的声明式路由方案,支持深层链接、重定向、嵌套路由等高级功能。
安装
bash
flutter pub add go_router基本用法
dart
import 'package:go_router/go_router.dart';
// 1. 定义路由
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/detail/:id', // 路径参数
builder: (context, state) {
final id = state.pathParameters['id']!;
return DetailPage(id: id);
},
),
GoRoute(
path: '/settings',
builder: (context, state) => SettingsPage(),
),
],
);
// 2. 在 MaterialApp 中使用
MaterialApp.router(
routerConfig: router,
)
// 3. 跳转
context.go('/detail/123'); // 替换当前路由
context.push('/detail/123'); // 压入新路由(可返回)
context.go('/settings'); // 跳转设置页
// 4. 返回
context.pop();路径参数与查询参数
dart
// 路径参数:定义时用 :变量名
GoRoute(
path: '/user/:id',
builder: (context, state) {
final id = state.pathParameters['id']; // 获取路径参数
return UserProfilePage(userId: id!);
},
)
// 查询参数:URL 中 ?key=value
context.go('/search?keyword=flutter&page=1');
// 获取查询参数
GoRoute(
path: '/search',
builder: (context, state) {
final keyword = state.uri.queryParameters['keyword'];
final page = state.uri.queryParameters['page'];
return SearchPage(keyword: keyword, page: int.tryParse(page ?? '1') ?? 1);
},
)嵌套路由(ShellRoute)
ShellRoute 可以在子路由之间共享布局(如底部导航栏):
dart
final router = GoRouter(
initialLocation: '/',
routes: [
ShellRoute(
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child); // 共享布局
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => HomePage(),
),
GoRoute(
path: '/explore',
builder: (context, state) => ExplorePage(),
),
GoRoute(
path: '/profile',
builder: (context, state) => ProfilePage(),
),
],
),
// 不带底部导航的页面
GoRoute(
path: '/detail/:id',
builder: (context, state) => DetailPage(id: state.pathParameters['id']!),
),
],
);共享布局组件:
dart
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedIndex(context),
onDestinationSelected: (index) {
switch (index) {
case 0: context.go('/'); break;
case 1: context.go('/explore'); break;
case 2: context.go('/profile'); break;
}
},
destinations: [
NavigationDestination(icon: Icon(Icons.home), label: '首页'),
NavigationDestination(icon: Icon(Icons.explore), label: '发现'),
NavigationDestination(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
int _selectedIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.path;
if (location.startsWith('/explore')) return 1;
if (location.startsWith('/profile')) return 2;
return 0;
}
}重定向
用于权限控制(如未登录时跳转登录页):
dart
final router = GoRouter(
redirect: (context, state) {
final isLoggedIn = AuthService.isLoggedIn;
final isLoginRoute = state.uri.path == '/login';
if (!isLoggedIn && !isLoginRoute) return '/login';
if (isLoggedIn && isLoginRoute) return '/';
return null; // 无需重定向
},
routes: [
GoRoute(path: '/', builder: (context, state) => HomePage()),
GoRoute(path: '/login', builder: (context, state) => LoginPage()),
GoRoute(path: '/settings', builder: (context, state) => SettingsPage()),
],
);错误处理
dart
final router = GoRouter(
errorBuilder: (context, state) => Scaffold(
appBar: AppBar(title: Text('页面未找到')),
body: Center(child: Text('404 - 页面不存在')),
),
routes: [...],
);go vs push 的区别
| 方法 | 行为 | 适用场景 |
|---|---|---|
context.go('/path') | 替换当前路由栈 | 底部导航切换、登录后跳转 |
context.push('/path') | 压入新路由 | 详情页、编辑页(需要返回) |
Navigator vs go_router
| 特性 | Navigator | go_router |
|---|---|---|
| 风格 | 命令式 | 声明式 |
| 深层链接 | 需手动处理 | 内置支持 |
| 嵌套路由 | 不支持 | ShellRoute |
| 重定向 | 手动实现 | 内置支持 |
| Web URL | 不支持 | 完整支持 |
| 学习成本 | 低 | 中 |
| 推荐程度 | 简单项目 | 正式项目(推荐) |
建议
- 学习阶段:先用
Navigator理解导航的基本概念 - 正式项目:使用
go_router,支持声明式路由、深层链接和 Web URL
