返回文章列表
Flutter 路由与导航完全指南:从基础到企业级架构
路由(Routing)和导航(Navigation)是 APP 的骨架,决定了用户如何在不同页面之间穿梭。Flutter 提供了多种导航方式:从最简单的 Navigator.push 到声明式的 GoRouter,再到复杂的嵌套导航。这篇文章覆盖从小型项目到企业级应用的所有路由场景。
一、Flutter 导航的三种方式
- 命令式导航(Imperative):Navigator.push/pop — 最直接,适合简单页面跳转
- 命名式路由(Named Routes):通过路由名称 + 参数跳转,适合中大型项目
- 声明式路由(Declarative / GoRouter):用 Widget 声明式定义路由,最适合大型项目
二、命令式导航基础
// ===== 最基本的页面跳转 =====
// 跳转并等待返回结果
final result = await Navigator.push(context,
MaterialPageRoute(builder: (_) => const DetailPage(id: 42)),
);
// 返回上一页
Navigator.pop(context);
// 返回时携带数据
Navigator.pop(context, '这是返回的数据');
// 替换当前页面(不能返回)
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const LoginPage()));
// 清空栈并跳转(常用于登出)
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const HomePage()),
(route) => route.isFirst, // 只保留首页,移除其他所有页面
);
// 弹出新页面(底部半屏弹窗等)
showModalBottomSheet(
context: context,
builder: (context) => Container(height: 300, color: Colors.white, child: Text('底部弹窗')),
);
三、命名式路由(Named Routes)
命名路由将路由名称集中管理,实现路由解耦。
3.1 定义路由表
// lib/app_routes.dart
class AppRoutes {
static const String home = '/';
static const String login = '/login';
static const String register = '/register';
static const String profile = '/profile';
static const String postDetail = '/post/:id';
static const String settings = '/settings';
}
class AppRouter {
static final Map<String, WidgetBuilder> routes = {
AppRoutes.home: (context) => const HomePage(),
AppRoutes.login: (context) => const LoginPage(),
AppRoutes.register: (context) => const RegisterPage(),
AppRoutes.profile: (context) => const ProfilePage(),
AppRoutes.settings: (context) => const SettingsPage(),
};
// 动态路由(需要参数的路由)
static Route<dynamic> onGenerateRoute(RouteSettings settings) {
final uri = Uri.parse(settings.name!);
if (uri.path == AppRoutes.postDetail) {
final id = uri.queryParameters['id'] ?? '';
return MaterialPageRoute(builder: (_) => PostDetailPage(postId: id));
}
// 未找到路由 → 返回 null 或 404 页面
return null;
}
}
3.2 在 MaterialApp 中注册
MaterialApp(
title: 'MyApp',
initialRoute: '/',
routes: AppRouter.routes,
onGenerateRoute: AppRouter.onGenerateRoute,
// 全局未知路由拦截
onUnknownRoute: (settings) => MaterialPageRoute(
builder: (_) => const NotFoundPage(),
),
)
3.3 使用命名路由跳转
// 简单跳转
Navigator.pushNamed(context, AppRoutes.settings);
// 携带参数
Navigator.pushNamed(
context,
AppRoutes.postDetail,
arguments: {'postId': '123', 'title': '文章标题'},
);
// 在目标页面获取参数
class PostDetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 方式一:ModalRoute 的 arguments
final args = ModalRoute.of(context)?.settings.arguments as Map?;
print(args?['postId']);
// 方式二:onGenerateRoute 中解析的路径参数
final routeArgs = ModalRoute.of(context);
final postId = routeArgs?.settings.name?.split('/').last;
return Scaffold(/* ... */);
}
}
四、路由守卫(Auth Guard)
实际项目中,很多页面需要登录才能访问。路由守卫在跳转到目标页面前检查登录状态,未登录则重定向到登录页。
// lib/guards/auth_guard.dart
import 'package:flutter/foundation.dart';
/// 认证守卫:在跳转前检查登录状态
class AuthGuard {
/// 需要认证的页面列表
static const List<String> _protectedRoutes = [
'/profile',
'/settings',
'/order',
];
/// 检查是否已认证
static Future<bool> isAuthenticated() async {
final token = await StorageUtil.getToken();
return token != null && token.isNotEmpty;
}
/// 守卫逻辑
static Future<String?> check(RouteSettings settings) async {
if (_protectedRoutes.any((r) => settings.name!.contains(r))) {
if (!(await isAuthenticated())) {
// 未登录 → 重定向到登录页,记录原始目标
return '${AppRoutes.login}?redirect=${Uri.encodeComponent(settings.name!)}';
}
}
return null; // 已认证,放行
}
/// 在 MaterialApp 的 onGenerateRoute 中使用
static Route<dynamic> guardNavigation(RouteSettings settings) async {
final redirect = await check(settings);
if (redirect != null) {
return MaterialPageRoute(builder: (_) => LoginPage(redirectTo: redirect));
}
return null;
}
}
五、深链接(Deep Link)
让外部链接可以直接打开 APP 内的指定页面。
// Android: android/app/src/main/AndroidManifest.xml
// <activity ...>
// <intent-filter>
// <action android:name="android.intent.action.VIEW"/>
// <category android:name="android.intent.category.DEFAULT"/>
// <category android:name="android.intent.category.BROWSABLE"/>
// <data android:scheme="https" android:host="myapp.com"/>
// </intent-filter>
// </activity>
// iOS: Runner/Info.plist
// CFBundleURLTypes
// Item 0
// CFBundleURLName = com.myapp.deeplink
// CFBundleURLSchemes = (myapp)
// CFBundleURLTypeIdentifier = myapp.deeplink
// CFBundleURLRole = Editor
// Flutter 端处理
import 'package:uni_links/uni_links.dart';
Future<void> initDeepLinks() async {
final appLinks = UniLinks();
appLinks.linkStream.listen((Uri link) {
// https://myapp.com/post/123
switch (link.path) {
case '/post':
final postId = link.queryParameters['id'];
navigatorKey.currentState?.pushNamed('/post/$postId');
break;
case '/user':
navigatorKey.currentState?.pushNamed('/profile');
break;
default:
// 打开首页
navigatorKey.currentState?.pushNamed('/');
}
});
}
六、Nested Navigation(嵌套导航)
当 APP 有底部 Tab 导航,而每个 Tab 又有自己的子页面栈时,就需要嵌套导航。典型场景:Tab A 有 3 个子页面,Tab B 有 5 个子页面。
class MainTabPage extends StatefulWidget {
@override
_MainTabPageState createState() => _MainTabPageState();
}
class _MainTabPageState extends State<MainTabPage> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 4, vsync: this);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: NestedNavigator(
key: const ValueKey('mainTab'),
initialRoute: '/',
observers: [_tabController],
child: TabBarView(
controller: _tabController,
children: [
// 每个 Tab 有自己独立的 Navigator 和页面栈
Navigator(
key: const ValueKey('homeNav'),
initialRoute: '/',
onGenerateRoute: (settings) {
if (settings.name == '/') {
return MaterialPageRoute(builder: (_) => const HomeFeedPage());
} else if (settings.name == '/detail') {
return MaterialPageRoute(builder: (_) => DetailPage());
}
return null;
},
),
Navigator(key: const ValueKey('searchNav'), initialRoute: '/',
onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => const SearchPage())),
Navigator(key: const ValueKey('notifyNav'), initialRoute: '/',
onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => const NotifyPage())),
Navigator(key: const ValueKey('mineNav'), initialRoute: '/',
onGenerateRoute: (_) => MaterialPageRoute(builder: (_) => const MinePage())),
],
),
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _tabController.index,
onTap: (i) {
_tabController.animateTo(i);
// 切换 Tab 时通知对应的 NestedNavigator
NestedNavigators.of(context).key.currentState?.popUntil((r) => r.isFirst);
},
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: '发现'),
BottomNavigationBarItem(icon: Icon(Icons.notifications), label: '消息'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
}
// 在 HomeFeedPage 中可以独立 push 子页面而不影响 Tab 切换
// 注意:必须使用 Navigator.of(context, rootNavigator: false) 来使用子 Navigator
Navigator.of(context, rootNavigator: false).pushNamed('/detail');
七、GoRouter — 企业级声明式路由
GoRouter 是社区最推荐的企业级路由方案,完全声明式定义,支持类型安全、深度链接、动画定制等功能。
// pubspec.yaml
dependencies:
go_router: ^14.0.0
// lib/router/router_config.dart
part of './app_router.dart';
// 定义路由枚举(类型安全)
enum AppRoute { home, login, profile, postDetail, settings }
// GoRouter 配置
final goRouter = GoRouter(
initialLocation: '/',
debugLogDiagnostics: true,
routes: [
GoRoute(
path: '/',
name: AppRoute.home.name,
builder: () => const HomePage(),
),
GoRoute(
path: '/login',
name: AppRoute.login.name,
builder: () => const LoginPage(),
),
GoRoute(
path: '/profile',
name: AppRoute.profile.name,
builder: () => const ProfilePage(),
// 路由守卫
redirect: (_, __) async {
final isLoggedIn = await StorageUtil.isLoggedIn();
return isLoggedIn ? null : const Redirect(AppRoute.login.name);
},
),
GoRoute(
path: '/post/:id',
name: AppRoute.postDetail.name,
builder: () => const PostDetailPage(),
),
],
// 错误页面
errorBuilder: (error, stack) => ErrorScreen(error: error, stack: stack),
);
// 使用方式 —— 类型安全的导航
context.push(AppRoute.profile.name); // 跳转到个人中心
context.pushNamed(AppRoute.postDetail.name, pathParams: {'id': '42'}); // 带参数
context.go('/post/42'); // 直接用路径也支持
// 带查询参数
context.go(Uri(path: '/search', queryParameters: {'q': 'flutter'}));
// Shell 路由(带底部导航的布局)
final shellRouter = GoRouter(
routes: [],
shellRoute: StatefulShellRoute.indexStack(
builder: (shellContext, state, child) => MainShell(child: child),
branches: [
StatefulShellBranch(
routes: [
GoRoute(path: '/', page: () => HomeShellPage()),
GoRoute(path: '/detail', page: () => DetailShellPage()),
],
),
],
),
);
八、路由最佳实践清单
- 统一管理路由名称:用常量或枚举集中定义,避免硬编码字符串
- 参数传递优先用构造函数:比 arguments 更类型安全
- 复杂对象传参用类实例:不要把大对象序列化为 JSON 字符串
- 路由守卫要处理循环重定向:避免 登录页 → 检查未登录 → 再到登录页 的死循环
- Nested Navigation 用 GlobalKey 区分:确保操作的是正确的 Navigator 栈
- 深链接要做合法性校验:防止恶意链接打开非法页面
- 过渡动画保持一致:自定义 PageTransitionsBuilder 统一风格
- Web 平台考虑 URL 同步:地址栏显示正确的路径
总结
Flutter 的导航体系非常灵活:个人项目用 Navigator.push/pop 就够用了;中型项目引入命名路由+路由守卫;大型项目上 GoRouter 声明式方案。关键不是选哪种框架,而是提前规划好路由结构,让整个 APP 的页面流转清晰可控。好的路由设计能让代码更易维护,用户体验也更流畅。