一文掌握Flutter路由管理:深入浅出
前言:Flutter 应用中,路由(Routing)即页面导航,是构建多页面体验的核心能力。从最简单的 Navigator.push
,到命名路由、参数传递、返回结果,再到集中式路由管理、深层嵌套与 Navigator 2.0(Router API),本文将以“从零到一”的思路,配合代码示例和图解,帮助你快速掌握 Flutter 的路由管理技巧。
目录
- 1.1
Navigator.push
与Navigator.pop
- 1.2 参数传递与返回结果
- 1.1
- 2.1 在
MaterialApp
中配置routes
- 2.2 使用
onGenerateRoute
实现动态路由 - 2.3 参数解读与示例
- 2.1 在
- 3.1
RouteObserver
与RouteAware
- 3.2 页面进入/退出回调场景
- 3.1
- 4.1 登录鉴权方案示例
- 4.2 利用
onGenerateRoute
与arguments
实现守卫
- 5.1 底部导航栏与独立导航栈
- 5.2 TabBar +
IndexedStack
+ 子Navigator
- 5.3 图解示意
- 6.1 为什么要 Navigator 2.0
- 6.2 核心概念:
Router
、RouteInformationParser
、RouterDelegate
- 6.3 简单示例:URL 与页面状态同步
- 7.1 功能需求与思路
- 7.2 代码实现:Navigator 1.0 版本
- 7.3 进阶:Navigator 2.0 版本(Router API)
- 总结与最佳实践
一、Flutter 路由基础:Navigator 1.0
在早期的 Flutter 中,最常见的页面跳转方式即基于 Navigator 1.0 API:Navigator.push
、Navigator.pop
。
1.1 Navigator.push
与 Navigator.pop
Navigator.push
:在页面栈顶压入一个新路由(新页面)。Navigator.pop
:从页面栈顶弹出当前路由,回到上一个页面(或传递返回值)。
// MainPage.dart
import 'package:flutter/material.dart';
import 'detail_page.dart';
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('MainPage')),
body: Center(
child: ElevatedButton(
child: const Text('跳转到 DetailPage'),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (ctx) => DetailPage()),
);
},
),
),
);
}
}
// DetailPage.dart
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('DetailPage')),
body: Center(
child: ElevatedButton(
child: const Text('返回 MainPage'),
onPressed: () {
Navigator.pop(context);
},
),
),
);
}
}
运行流程:
- 应用启动后,
MainPage
显示。 - 点击按钮后,
Navigator.push
将DetailPage
放入堆栈,屏幕切换到DetailPage
。 - 在
DetailPage
中点击“返回”,执行Navigator.pop(context)
,该路由出栈,自动回到MainPage
。
- 应用启动后,
路由栈示意图(ASCII)
初始状态:
┌────────────┐
│ MainPage │ ← 当前显示
└────────────┘
点击 push:
┌────────────┐
│ DetailPage │ ← 当前显示
└────────────┘
┌────────────┐
│ MainPage │
└────────────┘
点击 pop:
┌────────────┐
│ MainPage │ ← 当前显示
└────────────┘
1.2 参数传递与返回结果
1.2.1 从 MainPage 传参数到 DetailPage
// MainPage.dart
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => DetailPage(message: 'Hello, Detail!'),
),
);
// DetailPage.dart
class DetailPage extends StatelessWidget {
final String message;
DetailPage({required this.message});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('DetailPage')),
body: Center(
child: Text('接收到的参数:$message'),
),
);
}
}
- 重点:通过路由构造函数接收外部传递的参数。
1.2.2 从 DetailPage 返回带结果值到 MainPage
// MainPage.dart (修改 onPressed 部分)
onPressed: () async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
// 接收返回值
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('DetailPage 返回:$result')),
);
},
// DetailPage.dart (在返回按钮中传递结果)
ElevatedButton(
child: const Text('返回并带参'),
onPressed: () {
Navigator.pop(context, '这个是返回值');
},
),
流程:
MainPage
通过await Navigator.push(...)
等待DetailPage
出栈并返回一个值。- 在
DetailPage
调用Navigator.pop(context, someValue)
时,将someValue
传回给上一个页面。 MainPage
拿到result
后,可以做后续逻辑(如弹窗、更新状态等)。
二、命名路由与集中式路由表
随着页面增多,若应用中到处写 MaterialPageRoute(builder: …)
,会显得冗余且难维护。于是可以使用命名路由(named routes)和集中式路由表,由 MaterialApp
一次性注册所有路由,并用名称跳转。
2.1 在 MaterialApp
中配置 routes
// main.dart
import 'package:flutter/material.dart';
import 'pages/home_page.dart';
import 'pages/login_page.dart';
import 'pages/profile_page.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '命名路由示例',
initialRoute: '/', // 默认路由
routes: {
'/': (context) => const HomePage(),
'/login': (context) => const LoginPage(),
'/profile': (context) => const ProfilePage(),
},
);
}
}
路由表:
routes: { '/': (context) => const HomePage(), '/login': (context) => const LoginPage(), '/profile': (context) => const ProfilePage(), }
/
:代表应用启动后的初始页面。/login
、/profile
:其余命名路由。
在页面中跳转
// HomePage.dart
ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/login');
},
child: const Text('去登录'),
),
Navigator.pushNamed(context, '/login')
:根据路由表,将/login
对应的LoginPage
推入栈顶。
2.2 使用 onGenerateRoute
实现动态路由
当希望在跳转时传递参数、或者根据某些条件决定跳转目标时,可使用 onGenerateRoute
回调,手动创建路由。
// main.dart
MaterialApp(
initialRoute: '/',
onGenerateRoute: (RouteSettings settings) {
final name = settings.name;
final args = settings.arguments;
if (name == '/') {
return MaterialPageRoute(builder: (_) => const HomePage());
} else if (name == '/detail') {
// 从 arguments 中取出传递的参数
final id = args as int;
return MaterialPageRoute(builder: (_) => DetailPage(id: id));
} else {
// 未知路由,跳转到 404 页面
return MaterialPageRoute(builder: (_) => const NotFoundPage());
}
},
)
- RouteSettings:包含
name
(路由名)和arguments
(动态传参)。 示例跳转并传参:
Navigator.pushNamed(context, '/detail', arguments: 42);
在
onGenerateRoute
中判断:if (settings.name == '/detail') { final id = settings.arguments as int; return MaterialPageRoute(builder: (_) => DetailPage(id: id)); }
2.3 参数解读与示例
// DetailPage.dart
class DetailPage extends StatelessWidget {
final int id;
const DetailPage({required this.id});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('DetailPage')),
body: Center(child: Text('详情 ID:$id')),
);
}
}
跳转:
ElevatedButton( onPressed: () { Navigator.pushNamed(context, '/detail', arguments: 123); }, child: const Text('查看 ID=123 的详情'), );
路由栈示意:
HomePage → (Navigator.pushNamed '/detail', arguments=123) → DetailPage(id=123)
三、路由观察与页面生命周期
在某些场景下,需要监听页面何时被 push、pop、onResume、onPause 等,例如埋点、分析统计、清理资源等。这时可以借助 RouteObserver
与 RouteAware
。
3.1 RouteObserver
与 RouteAware
RouteObserver
:一个拥有回调的路由观察者,需在MaterialApp
中注册。RouteAware
:页面(Widget)实现该接口,即可接收路由切换事件。
3.1.1 在 MaterialApp
中注册 RouteObserver
// main.dart
final RouteObserver<PageRoute> routeObserver = RouteObserver<PageRoute>();
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...
navigatorObservers: [routeObserver], // 注册路由观察器
// ...
);
}
}
3.1.2 页面实现 RouteAware
// ProfilePage.dart
import 'package:flutter/material.dart';
class ProfilePage extends StatefulWidget {
const ProfilePage();
@override
_ProfilePageState createState() => _ProfilePageState();
}
class _ProfilePageState extends State<ProfilePage> with RouteAware {
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 在页面依赖更改时,注册订阅
MyApp.routeObserver.subscribe(this, ModalRoute.of(context)! as PageRoute);
}
@override
void dispose() {
// 注销订阅
MyApp.routeObserver.unsubscribe(this);
super.dispose();
}
@override
void didPush() {
// 当前路由被 push 到栈顶(首次进入)
debugPrint('ProfilePage: didPush');
}
@override
void didPopNext() {
// 栈顶路由 popped,当前路由重新可见(相当于 resume)
debugPrint('ProfilePage: didPopNext (resumed)');
}
@override
void didPushNext() {
// 当前路由被新的路由覆盖(相当于 pause)
debugPrint('ProfilePage: didPushNext (paused)');
}
@override
void didPop() {
// 当前路由被 pop
debugPrint('ProfilePage: didPop');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ProfilePage')),
body: const Center(child: Text('个人中心页面')),
);
}
}
关键回调:
didPush()
:页面第一次被 push 时调用。didPopNext()
:当下一个页面 pop 后,当前页面重新可见时调用。didPushNext()
:当当前页面上再 push 出新页面,当前页面不可见时调用。didPop()
:当当前页面被 pop 时调用。
3.2 页面进入/退出回调场景
- 场景 1:数据埋点
在didPush()
中触发“页面露出”统计,在didPop()
中触发“页面消失”统计。 - 场景 2:播放/暂停资源
如果页面里有视频播放器,didPush()
开始播放,didPushNext()
暂停播放;didPopNext()
恢复播放。 - 场景 3:实时刷新
当用户在 A 页面点击按钮跳到 B 页面,并在 B 页面做了设置,回到 A 页面时需要刷新列表,可在 A 的didPopNext()
中触发网络请求。
四、路由守卫与拦截(Route Guard)
在实际项目中,经常需要在用户未登录时,限制访问某些页面;或根据某些权限动态决定跳转目标。这时就需要在路由层做守卫或拦截。
4.1 登录鉴权方案示例
思路:
- 用户点击“进入个人中心”时,先判断是否已登录。
- 若未登录,则跳转到登录页(
/login
)。 - 登录成功后,再跳回“个人中心”或继续原先请求。
4.1.1 在 onGenerateRoute
中实现
// main.dart
import 'package:flutter/material.dart';
bool isLoggedIn = false; // 模拟登录状态
Route<dynamic> onGenerateRoute(RouteSettings settings) {
final name = settings.name;
final args = settings.arguments;
// 如果要进入 /profile,需要先判断登录状态
if (name == '/profile') {
if (!isLoggedIn) {
// 未登录,先去登录页,并把目标页面信息放在 arguments 中,登录完成后再跳转
return MaterialPageRoute(
builder: (_) => LoginPage(targetRoute: '/profile'),
);
}
// 已登录,正常展示 ProfilePage
return MaterialPageRoute(builder: (_) => const ProfilePage());
}
// 登录页
if (name == '/login') {
final target = (args as String?);
return MaterialPageRoute(builder: (_) => LoginPage(targetRoute: target));
}
// 默认路由
return MaterialPageRoute(builder: (_) => const HomePage());
}
class LoginPage extends StatelessWidget {
final String? targetRoute;
const LoginPage({this.targetRoute});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('LoginPage')),
body: Center(
child: ElevatedButton(
child: const Text('模拟登录'),
onPressed: () {
isLoggedIn = true; // 登录成功
// 登录成功后,跳转到目标路由(如果有)
if (targetRoute != null) {
Navigator.pushReplacementNamed(context, targetRoute!);
} else {
Navigator.pop(context);
}
},
),
),
);
}
}
流程:
- 当用户
Navigator.pushNamed(context, '/profile')
时,onGenerateRoute
发现isLoggedIn = false
,先跳到LoginPage
,并记录目标路由'/profile'
。 - 在
LoginPage
中,点击“模拟登录”后,设isLoggedIn=true
,再执行pushReplacementNamed('/profile')
,直接替换当前登录页,进入个人中心。
- 当用户
4.2 利用 onGenerateRoute
与 arguments
实现守卫
- 示例中重点:通过
RouteSettings.arguments
将“原始目标路由”传递给登录页。 - 扩展思路:你也可以在
onGenerateRoute
里判断用户权限等级、角色等,决定能否访问某些敏感页面。
五、嵌套路由与多 Navigator 场景
在一些复杂 UI(如带底部导航栏、TabBar)中,需要在各个 Tab 内维护独立的导航栈,这时就要用到嵌套路由。
5.1 底部导航栏与独立导航栈
- 场景:一个含有底部导航的应用,共有 3 个 Tab(Home、Discovery、Profile)。希望切换 Tab 时,各自的页面栈保持独立状态(例如:在 HomeTab 内从 A → B 页面,然后切到 Discovery,再切回 Home 时仍然在 B 页面)。
5.1.1 核心思想
- 在最外层的 Scaffold 里放置一个
IndexedStack
,用于保持各个子Navigator
的状态。 - 为每个 Tab 创建一个独立的
Navigator
,并用GlobalKey<NavigatorState>
来管理它的导航操作。
// tab_navigator.dart
import 'package:flutter/material.dart';
class TabNavigator extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey;
final String tabItem; // 'home', 'discover', 'profile'
const TabNavigator({required this.navigatorKey, required this.tabItem});
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
onGenerateRoute: (RouteSettings settings) {
Widget page;
if (settings.name == '/') {
switch (tabItem) {
case 'home':
page = const HomeTabPage();
break;
case 'discover':
page = const DiscoverTabPage();
break;
case 'profile':
page = const ProfileTabPage();
break;
default:
page = const HomeTabPage();
}
} else if (settings.name == '/detail') {
final args = settings.arguments as String;
page = DetailPage(data: args);
} else {
page = const HomeTabPage();
}
return MaterialPageRoute(builder: (_) => page);
},
);
}
}
// main.dart (底部导航 + IndexedStack)
import 'package:flutter/material.dart';
import 'tab_navigator.dart';
class MainScaffold extends StatefulWidget {
const MainScaffold();
@override
_MainScaffoldState createState() => _MainScaffoldState();
}
class _MainScaffoldState extends State<MainScaffold> {
int _currentIndex = 0;
// 为每个 Tab 准备一个 NavigatorKey
final Map<int, GlobalKey<NavigatorState>> _navigatorKeys = {
0: GlobalKey<NavigatorState>(),
1: GlobalKey<NavigatorState>(),
2: GlobalKey<NavigatorState>(),
};
void _onTap(int index) {
if (_currentIndex == index) {
// 若点中当前 Tab,且该栈不在根页,则 pop 到根
_navigatorKeys[index]!
.currentState!
.popUntil((route) => route.isFirst);
} else {
setState(() {
_currentIndex = index;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: [
TabNavigator(navigatorKey: _navigatorKeys[0]!, tabItem: 'home'),
TabNavigator(navigatorKey: _navigatorKeys[1]!, tabItem: 'discover'),
TabNavigator(navigatorKey: _navigatorKeys[2]!, tabItem: 'profile'),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: _onTap,
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: '发现'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
}
要点:
IndexedStack
:保持子 Widget 状态不被销毁。- 独立
Navigator
:每个 Tab 内维护自己的路由栈,互不干扰。 - 点击同一 Tab 时,如果子路由栈深度不为 1,则自动 pop 回根页面。
5.2 TabBar + IndexedStack
+ 子 Navigator
- 当顶部使用
TabBar
,但仍想保持每个 Tab 的导航状态,则思路几乎一致:只是将BottomNavigationBar
换成TabBar
,并配合TabController
。 - 利用
DefaultTabController
包裹整个 Scaffold,然后在TabBarView
每个子页面使用独立Navigator
。
// tabbar_navigator.dart
DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
title: const Text('TabBar 嵌套路由'),
bottom: const TabBar(
tabs: [
Tab(text: '新闻'),
Tab(text: '图片'),
Tab(text: '设置'),
],
),
),
body: TabBarView(
children: [
TabNavigator(navigatorKey: _navigatorKeys[0]!, tabItem: 'news'),
TabNavigator(navigatorKey: _navigatorKeys[1]!, tabItem: 'gallery'),
TabNavigator(navigatorKey: _navigatorKeys[2]!, tabItem: 'settings'),
],
),
),
);
5.3 图解示意
┌───────────────────────────────────────────┐
│ MainScaffold (Scaffold) │
│ ┌───────────────────────────────┐ │
│ │ IndexedStack │ │
│ │ ┌──────────┐ ┌──────────┐ ┌────────┐ │
│ │ │Navigator │ │Navigator │ │Navigator│ │
│ │ │ (Home) │ │ (Disc.) │ │ (Prof.) │ │
│ │ └──────────┘ └──────────┘ └────────┘ │
│ └───────────────────────────────┘ │
│ BottomNavigationBar (3 个 Tab) │
└───────────────────────────────────────────┘
说明:
- IndexedStack 的第 0 个子 Widget 是 Home Navigator,里面的路由栈可包含
/home/detail
、/home/settings
等。 - 切换到第 1 个 Tab 时,会在第二个 Navigator 中显示对应页面,并且不销毁第 0 个 Navigator 的状态。
- IndexedStack 的第 0 个子 Widget 是 Home Navigator,里面的路由栈可包含
六、Navigator 2.0(Router API)简介
自 Flutter 1.22 起,官方推出了全新的 Navigator 2.0(Router API),用于更好地支持Web URL 路由、深度链接(Deep Link)和灵活可控的路由栈。即使今天大多数移动 App 仍可用 Navigator 1.0,但对于需要与浏览器 URL 同步、或需要在恢复时重建路由栈的场景,Navigator 2.0 更具优势。
6.1 为什么要 Navigator 2.0
- URL 与页面状态双向绑定:在 Web 端,用户可以通过输入网址直接访问某个子页面,Router API 可以根据浏览器 URL 初始化路由。
- 可编程式路由栈控制:可以以声明式方式描述“当前应显示哪些页面”,无需手动
push
/pop
,更容易实现复杂场景。 - 页面恢复与深度链接:当 App 被系统杀死后重启,Router 可以根据原始路由信息自动“回到”之前的路由栈。
6.2 核心概念:Router
、RouteInformationParser
、RouterDelegate
RouteInformationParser
- 负责解析浏览器地址栏(
RouteInformation
)为应用内部可理解的“路由配置模型”(如一个枚举或自定义对象)。 - 例如将路径
/profile/123
解析到MyRoutePath.profile(123)
这样的模型。
- 负责解析浏览器地址栏(
RouterDelegate
- 根据“路由配置模型”来构建实际的页面栈(List
)。 需要实现:
List<Page> get currentConfiguration
:返回当前路由模型,用于同步给RouteInformationParser
。Widget build(BuildContext context)
:构建一个Navigator
,并把pages
列表传入,决定了实际的页面栈结构。Future<bool> popRoute()
:当按下 Android 返回键或浏览器后退时,处理 pop 操作并更新路由模型。
- 根据“路由配置模型”来构建实际的页面栈(List
Router
Widget- 用来实际渲染,由
Router.routerDelegate
和Router.routeInformationParser
配置,并在底层管理Navigator
。
- 用来实际渲染,由
6.3 简单示例:URL 与页面状态同步
以下示例演示一个简单的三页应用(Home → Profile(userId) → Settings),并将路径与页面状态关联。
6.3.1 定义路由模型
// route_path.dart
abstract class MyRoutePath {}
class HomePath extends MyRoutePath {}
class ProfilePath extends MyRoutePath {
final String userId;
ProfilePath(this.userId);
}
class SettingsPath extends MyRoutePath {}
6.3.2 实现 RouteInformationParser
// my_route_parser.dart
import 'package:flutter/material.dart';
import 'route_path.dart';
class MyRouteInformationParser extends RouteInformationParser<MyRoutePath> {
@override
Future<MyRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location ?? '/');
// /profile/123
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'profile') {
final userId = uri.pathSegments[1];
return ProfilePath(userId);
}
// /settings
if (uri.path == '/settings') {
return SettingsPath();
}
// 默认 /
return HomePath();
}
@override
RouteInformation restoreRouteInformation(MyRoutePath configuration) {
if (configuration is ProfilePath) {
return RouteInformation(location: '/profile/${configuration.userId}');
}
if (configuration is SettingsPath) {
return const RouteInformation(location: '/settings');
}
return const RouteInformation(location: '/');
}
}
6.3.3 实现 RouterDelegate
// my_router_delegate.dart
import 'package:flutter/material.dart';
import 'route_path.dart';
import 'pages/home_page.dart';
import 'pages/profile_page.dart';
import 'pages/settings_page.dart';
class MyRouterDelegate extends RouterDelegate<MyRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey;
MyRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
MyRoutePath _currentPath = HomePath();
MyRoutePath get currentConfiguration => _currentPath;
// 更新路由模型并通知重建
void _handlePath(MyRoutePath newPath) {
_currentPath = newPath;
notifyListeners();
}
// 处理 Android 实体返回键 / 浏览器后退
@override
Future<bool> popRoute() {
if (_currentPath is ProfilePath || _currentPath is SettingsPath) {
_currentPath = HomePath();
notifyListeners();
return Future.value(true);
}
return Future.value(false); // 不能 pop,交给系统
}
@override
Widget build(BuildContext context) {
// 根据 _currentPath 构建 Navigator.pages 列表
final pages = <Page>[
MaterialPage(child: HomePage(onProfile: (userId) {
_handlePath(ProfilePath(userId));
}, onSettings: () {
_handlePath(SettingsPath());
})),
];
if (_currentPath is ProfilePath) {
final userId = (_currentPath as ProfilePath).userId;
pages.add(MaterialPage(child: ProfilePage(userId: userId)));
}
if (_currentPath is SettingsPath) {
pages.add(MaterialPage(child: SettingsPage()));
}
return Navigator(
key: navigatorKey,
pages: pages,
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// 用户在页面上点击返回,更新路由模型
if (_currentPath is ProfilePath || _currentPath is SettingsPath) {
_currentPath = HomePath();
notifyListeners();
}
return true;
},
);
}
@override
Future<void> setNewRoutePath(MyRoutePath configuration) {
_currentPath = configuration;
return Future.value();
}
}
6.3.4 将 Router
挂载到 App
// main.dart
import 'package:flutter/material.dart';
import 'my_route_parser.dart';
import 'my_router_delegate.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationParser: MyRouteInformationParser(),
routerDelegate: MyRouterDelegate(),
title: 'Navigator 2.0 示例',
);
}
}
说明:
MaterialApp.router
指定了RouteInformationParser
与RouterDelegate
;- 当用户在浏览器地址栏输入
/profile/123
时,parseRouteInformation
会返回ProfilePath('123')
,随后RouterDelegate
将该路径映射到相应页面栈,并展示ProfilePage(userId: '123')
。 - 当在页面内部调用
_handlePath(SettingsPath())
时,currentConfiguration
会被更新为/settings
,自动同步到浏览器地址栏。
七、实战示例:构建一个简单的登录—主页—详情三层导航
下面以一个典型的“登录—主页—详情”示例,分别用 Navigator 1.0 与 Navigator 2.0 两种方式,实现路由管理,并对比两者的差异与优劣。
7.1 功能需求与思路
需求
- 用户打开应用后进入
LoginPage
。 - 登录成功后,跳转到
HomePage
。 - 在
HomePage
有一个列表,点击某行可进入DetailPage(itemId)
。 - 支持从 DetailPage 返回到 HomePage,并支持按物理“后退”键返回或退出应用。
- 支持深度链接:如果用户直接通过浏览器访问
/detail/42
,且已登录,则直接进入DetailPage(itemId=42)
;如果未登录,则先进入LoginPage
,登录成功后再自动跳转到该DetailPage
。
- 用户打开应用后进入
思路
- Navigator 1.0:使用
onGenerateRoute
做登录保护、参数解析,并在LoginPage
成功后手动管理跳转栈。深度链接支持不友好,需要外部插件或手动解析initialRoute
。 - Navigator 2.0:通过
RouteInformationParser
解析 URL,内置深度链接支持;RouterDelegate
统一根据登录状态和目标路由构建页面栈,逻辑更清晰、可维护。
- Navigator 1.0:使用
7.2 代码实现:Navigator 1.0 版本
7.2.1 定义页面文件
// login_page.dart
import 'package:flutter/material.dart';
bool isLoggedIn = false; // 全局模拟登录状态
class LoginPage extends StatelessWidget {
final String? targetRoute;
const LoginPage({this.targetRoute});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login')),
body: Center(
child: ElevatedButton(
child: const Text('登录'),
onPressed: () {
isLoggedIn = true;
// 登录后,如果有原始目标路由,则跳转该路由
if (targetRoute != null) {
Navigator.pushReplacementNamed(context, targetRoute!);
} else {
Navigator.pushReplacementNamed(context, '/home');
}
},
),
),
);
}
}
// home_page.dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
const HomePage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home')),
body: ListView.builder(
itemCount: 100,
itemBuilder: (ctx, i) {
return ListTile(
title: Text('Item $i'),
onTap: () {
Navigator.pushNamed(
context,
'/detail',
arguments: i,
);
},
);
},
),
);
}
}
// detail_page.dart
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
final int itemId;
const DetailPage({required this.itemId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail $itemId')),
body: Center(child: Text('详情项:$itemId')),
);
}
}
// not_found_page.dart
import 'package:flutter/material.dart';
class NotFoundPage extends StatelessWidget {
const NotFoundPage();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('404')),
body: const Center(child: Text('页面不存在')),
);
}
}
7.2.2 在 main.dart
中配置 onGenerateRoute
// main.dart (Navigator 1.0)
import 'package:flutter/material.dart';
import 'login_page.dart';
import 'home_page.dart';
import 'detail_page.dart';
import 'not_found_page.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Login-Home-Detail',
initialRoute: '/login',
onGenerateRoute: (settings) {
// 登录页
if (settings.name == '/login') {
return MaterialPageRoute(
builder: (_) => LoginPage(
targetRoute: settings.arguments as String?,
),
);
}
// Home 需要登录保护
if (settings.name == '/home') {
if (!isLoggedIn) {
// 未登录,跳到登录页,并把原始目标放到 arguments
return MaterialPageRoute(
builder: (_) => LoginPage(targetRoute: '/home'),
);
}
return MaterialPageRoute(builder: (_) => const HomePage());
}
// Detail 需要登录保护、同时要传参
if (settings.name == '/detail') {
if (!isLoggedIn) {
return MaterialPageRoute(
builder: (_) => LoginPage(targetRoute: '/detail'),
);
}
final args = settings.arguments;
if (args is int) {
return MaterialPageRoute(
builder: (_) => DetailPage(itemId: args),
);
}
return MaterialPageRoute(builder: (_) => const NotFoundPage());
}
// 未知路由
return MaterialPageRoute(builder: (_) => const NotFoundPage());
},
);
}
}
- 登录保护逻辑:在跳转
/home
、/detail
时,先检查isLoggedIn
,若为false
,则跳转登录页,并把目标路由信息放到参数targetRoute
。 - 深度链接(初次打开带参数):当用户直接通过外部调用
runApp
时指定initialRoute
(如/detail
),仍会进入onGenerateRoute
,通过相同逻辑进行登录检查与参数解析。
7.2.3 运行效果示意
- 用户未登录:应用启动,
initialRoute='/login'
,直接显示登录页。 - 在登录页点击“登录”:将
isLoggedIn=true
,跳转到/home
。 - 在 HomePage 点击某行(item=5):执行
Navigator.pushNamed('/detail', arguments: 5)
,且isLoggedIn=true
,进入DetailPage(itemId: 5)
。 - 在 DetailPage 点击返回:
Navigator.pop()
,回到 HomePage。 用户关闭 App,再次通过
/detail/10
打开:在runApp
时指定initialRoute='/detail'
,进入onGenerateRoute
,发现isLoggedIn=false
,先跳到登录页,并把目标路由参数传给LoginPage(targetRoute='/detail')
。在登录成功后,会
pushReplacementNamed('/detail')
,而此时settings.arguments
会为null
。如果想直接保留参数10
,则需要调整传递逻辑,例如将参数一起传给登录页:// 路径:'/detail/10' // 在 onGenerateRoute 里解析 uri.pathSegments final uri = Uri.parse(settings.name!); if (uri.pathSegments[0] == 'detail') { final id = int.tryParse(uri.pathSegments[1] ?? ''); if (!isLoggedIn) { return MaterialPageRoute( builder: (_) => LoginPage(targetRoute: '/detail', targetArgs: id), ); } if (id != null) { return MaterialPageRoute(builder: (_) => DetailPage(itemId: id)); } }
7.3 进阶:Navigator 2.0 版本(Router API)
下面用 Router API 重构上面的业务逻辑,实现同样的“深度链接 + 登录保护”功能。
7.3.1 定义路由路径模型
// my_route_path.dart
abstract class MyRoutePath {}
class LoginPath extends MyRoutePath {
final String? targetRoute;
final int? targetItemId;
LoginPath({this.targetRoute, this.targetItemId});
}
class HomePath extends MyRoutePath {}
class DetailPath extends MyRoutePath {
final int itemId;
DetailPath(this.itemId);
}
7.3.2 实现 RouteInformationParser
// my_route_parser.dart
import 'package:flutter/material.dart';
import 'my_route_path.dart';
class MyRouteParser extends RouteInformationParser<MyRoutePath> {
@override
Future<MyRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location ?? '/login');
// /login
if (uri.pathSegments.isEmpty || uri.path == '/login') {
return LoginPath();
}
// /home
if (uri.path == '/home') {
return HomePath();
}
// /detail/xx
if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'detail') {
final id = int.tryParse(uri.pathSegments[1]);
if (id != null) {
return DetailPath(id);
}
}
// 默认:LoginPath
return LoginPath();
}
@override
RouteInformation restoreRouteInformation(MyRoutePath configuration) {
if (configuration is HomePath) {
return const RouteInformation(location: '/home');
}
if (configuration is DetailPath) {
return RouteInformation(location: '/detail/${configuration.itemId}');
}
// LoginPath
return const RouteInformation(location: '/login');
}
}
7.3.3 实现 RouterDelegate
// my_router_delegate.dart
import 'package:flutter/material.dart';
import 'my_route_path.dart';
import 'pages/login_page.dart';
import 'pages/home_page.dart';
import 'pages/detail_page.dart';
class MyRouterDelegate extends RouterDelegate<MyRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<MyRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey;
bool _isLoggedIn = false;
MyRoutePath _currentPath = LoginPath();
MyRouterDelegate() : navigatorKey = GlobalKey<NavigatorState>();
@override
MyRoutePath get currentConfiguration {
return _currentPath;
}
Future<void> _handleLogin({String? targetRoute, int? targetItemId}) async {
_isLoggedIn = true;
if (targetRoute == '/detail' && targetItemId != null) {
_currentPath = DetailPath(targetItemId);
} else {
_currentPath = HomePath();
}
notifyListeners();
}
@override
Widget build(BuildContext context) {
List<Page> pages = [];
// 未登录时,只显示登录页
if (!_isLoggedIn) {
final loginPath = _currentPath is LoginPath
? _currentPath as LoginPath
: LoginPath();
pages.add(
MaterialPage(
child: LoginPage(
onLogin: () => _handleLogin(
targetRoute: loginPath.targetRoute,
targetItemId: loginPath.targetItemId,
),
),
),
);
} else {
// 登录后,至少要有 HomePage
pages.add(MaterialPage(child: HomePage(onItemTap: (id) {
_currentPath = DetailPath(id);
notifyListeners();
})));
// 如果目标是 Detail,则再压入 DetailPage
if (_currentPath is DetailPath) {
final itemId = (_currentPath as DetailPath).itemId;
pages.add(MaterialPage(child: DetailPage(itemId: itemId)));
}
}
return Navigator(
key: navigatorKey,
pages: pages,
onPopPage: (route, result) {
if (!route.didPop(result)) {
return false;
}
// 用户点击返回键
if (_currentPath is DetailPath) {
_currentPath = HomePath();
notifyListeners();
return true;
}
// 如果在 Home 页面按返回,退出 App
return false;
},
);
}
@override
Future<void> setNewRoutePath(MyRoutePath configuration) async {
// 当系统或浏览器传入新路由信息时触发,比如 URL 发生变化
if (configuration is LoginPath) {
_isLoggedIn = false;
_currentPath = configuration;
} else if (configuration is HomePath) {
if (_isLoggedIn) {
_currentPath = configuration;
} else {
// 未登录情况,重定向到登录,并保存目标
_currentPath = LoginPath(targetRoute: '/home');
}
} else if (configuration is DetailPath) {
if (_isLoggedIn) {
_currentPath = configuration;
} else {
_currentPath = LoginPath(
targetRoute: '/detail', targetItemId: configuration.itemId);
}
}
notifyListeners();
}
}
7.3.4 LoginPage
与 HomePage
作相应修改
// pages/login_page.dart
import 'package:flutter/material.dart';
class LoginPage extends StatelessWidget {
final VoidCallback onLogin;
const LoginPage({required this.onLogin});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Login (Navigator 2.0)')),
body: Center(
child: ElevatedButton(
child: const Text('登录'),
onPressed: onLogin,
),
),
);
}
}
// pages/home_page.dart
import 'package:flutter/material.dart';
class HomePage extends StatelessWidget {
final Function(int) onItemTap;
const HomePage({required this.onItemTap});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Home (Navigator 2.0)')),
body: ListView.builder(
itemCount: 20,
itemBuilder: (ctx, i) {
return ListTile(
title: Text('Item $i'),
onTap: () => onItemTap(i),
);
},
),
);
}
}
// pages/detail_page.dart
import 'package:flutter/material.dart';
class DetailPage extends StatelessWidget {
final int itemId;
const DetailPage({required this.itemId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Detail (Navigator 2.0): $itemId')),
body: Center(child: Text('详情内容:$itemId')),
);
}
}
7.3.5 将 Router
挂载到 App
// main.dart (Navigator 2.0)
import 'package:flutter/material.dart';
import 'my_route_parser.dart';
import 'my_router_delegate.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: MyRouterDelegate(),
routeInformationParser: MyRouteParser(),
title: 'Login-Home-Detail (Navigator 2.0)',
);
}
}
运行效果:
- 直接在浏览器(或模拟环境)输入
http://localhost:xxxx/detail/5
。 RouteInformationParser.parseRouteInformation
解析到DetailPath(5)
,传给setNewRoutePath
。setNewRoutePath
检测到用户未登录,则设置_currentPath = LoginPath(targetRoute='/detail', targetItemId=5)
。build()
构建时,_isLoggedIn=false
,只显示登录页;登录成功后触发_handleLogin(targetRoute='/detail', targetItemId=5)
,将_currentPath = DetailPath(5)
,即时 rebuild,跳转到DetailPage(itemId=5)
。- 当用户在
DetailPage
点击返回(Android 系统返回键),触发popRoute()
→_currentPath = HomePath()
→ rebuild 返回HomePage
。
- 直接在浏览器(或模拟环境)输入
八、总结与最佳实践
Navigator 1.0 适合简单场景
- 只需用
Navigator.push
/pop
即可,代码量少,上手快。 - 若只需移动端,并且不关心浏览器 URL 或深度链接,可优先使用。
- 只需用
命名路由与集中式管理
- 当项目页面众多时,可通过
routes
或onGenerateRoute
把路由集中管理,提高可维护性。 onGenerateRoute
更加灵活,可动态解析参数并做鉴权拦截。
- 当项目页面众多时,可通过
Route 观察与页面生命周期
- 用
RouteObserver
+RouteAware
可以监听页面进入/退出,适合做统计、业务逻辑触发。
- 用
嵌套路由与多导航栈
- 当需要底部导航栏、侧边导航或 Tab 组合时,可用多个
Navigator
嵌套,配合IndexedStack
保持各自状态。 - 核心要点在于给每个子
Navigator
分配独立的GlobalKey<NavigatorState>
。
- 当需要底部导航栏、侧边导航或 Tab 组合时,可用多个
Navigator 2.0(Router API)适用于需要 Web 支持与深度链接的场景
- 通过
RouteInformationParser
和RouterDelegate
分离“URL 解析”和“页面栈构建”两大职责,易于测试与扩展。 - 宣告式地根据路由模型构建页面栈,更容易实现“根据状态渲染页面”的思路。
- 学习曲线比 Navigator 1.0 更陡,但一旦掌握,可支持更复杂的路由需求。
- 通过
最佳实践要点
- 优先考虑 Navigator 1.0,在移动端小型 App 中满足大多数需求;
- 如需深度链接,先采用
onGenerateRoute
做 URI 解析,再考虑全面切换到 Navigator 2.0; - 定期清理路由栈,避免过深的导航链导致内存占用和页面恢复问题;
- 登录/鉴权 等通用规则,可在
onGenerateRoute
里实现一次性拦截,避免在每个页面中都写重复逻辑; - RouteObserver 和 嵌套路由 常结合使用,解决复杂场景下的页面状态管理与事件监听。
至此,你已经从最基础的 Navigator.push / pop
,到命名路由集中式管理,再到嵌套路由,乃至 Navigator 2.0 的 Router API 全面扫盲。无论是简单移动端 App,还是需要 Web URL 同步与深度链接的跨平台应用,都可以在本文范式中找到相应的最佳实践。
评论已关闭