Back to listFlutter Navigator 2.0 与声明式路由深度解析
Site Owner
Published on 2026-05-22
详解Flutter声明式路由架构、Pages API、RouterDelegate实现、深链接处理及go_router高级用法
Flutter Navigator 2.0 与声明式路由深度解析 (Declarative Routing)
前言
Navigator 1.0 的命令式 API(push/pop)在移动端 App 中运作良好,但当 Flutter 需要支持 Web 时,暴露出明显的缺陷:
- URL 不同步:用户通过代码 push 页面,浏览器地址栏无法同步更新
- 无法处理浏览器的前进/后退:浏览器的历史导航无法映射到 Flutter 的路由栈
- Deep Link 处理困难:从外部打开特定页面时,无法声明式地描述页面栈
- 无法序列化路由状态:路由栈是命令式操作的结果,难以持久化
Navigator 2.0(又称 Router API)通过引入声明式的页面栈管理来解决这些问题。
一、Navigator 2.0 的三角架构
┌─────────────────────┐
URL/DeepLink ───▶ │ RouteInformationParser│ ──解析──▶ 应用状态(AppState)
└─────────────────────┘
↑ 反向同步
┌─────────────────────┐
应用状态 ────────▶ │ RouterDelegate │ ──生成──▶ List<Page>(页面栈)
└─────────────────────┘
│
┌─────────────────────┐
│ Navigator │ ──渲染──▶ UI
└─────────────────────┘
三个核心组件:
| 组件 | 职责 |
|---|
RouteInformationParser | 将 URL/DeepLink 解析为应用状态(AppState);将应用状态序列化为 URL |
RouterDelegate | 根据应用状态构建 List<Page>(声明式的页面栈);监听状态变化并通知 Router 重建 |
Navigator | 接收 pages 列表,渲染对应的页面栈 |
二、手写 Navigator 2.0(理解原理)
Flutter Navigator 2.0声明式路由—Router API与go_router实战|新宇宙博客为了真正理解 Navigator 2.0,我们先不用任何库,手写一个完整实现:
2.1 定义应用状态
// 路由状态:描述当前的页面栈
class AppRouteState {
final bool isLoggedIn;
final String? selectedProductId;
final bool isSettingsOpen;
const AppRouteState({
this.isLoggedIn = false,
this.selectedProductId,
this.isSettingsOpen = false,
});
AppRouteState copyWith({
bool? isLoggedIn,
String? selectedProductId,
bool? isSettingsOpen,
bool clearProduct = false,
}) {
return AppRouteState(
isLoggedIn: isLoggedIn ?? this.isLoggedIn,
selectedProductId: clearProduct ? null : (selectedProductId ?? this.selectedProductId),
isSettingsOpen: isSettingsOpen ?? this.isSettingsOpen,
);
}
}
class AppRouteInformationParser extends RouteInformationParser<AppRouteState> {
@override
Future<AppRouteState> parseRouteInformation(
RouteInformation routeInformation,
) async {
final uri = Uri.parse(routeInformation.location ?? '/');
// 根据 URL 路径解析为应用状态
if (uri.pathSegments.isEmpty) {
return const AppRouteState(isLoggedIn: true);
}
switch (uri.pathSegments[0]) {
case 'login':
return const AppRouteState(isLoggedIn: false);
case 'product':
if (uri.pathSegments.length >= 2) {
return AppRouteState(
isLoggedIn: true,
selectedProductId: uri.pathSegments[1],
);
}
return const AppRouteState(isLoggedIn: true);
case 'settings':
return const AppRouteState(isLoggedIn: true, isSettingsOpen: true);
default:
return const AppRouteState(isLoggedIn: true);
}
}
@override
RouteInformation? restoreRouteInformation(AppRouteState configuration) {
// 将应用状态序列化为 URL(用于浏览器地址栏同步)
if (!configuration.isLoggedIn) {
return const RouteInformation(location: '/login');
}
if (configuration.selectedProductId != null) {
return RouteInformation(
location: '/product/${configuration.selectedProductId}',
);
}
if (configuration.isSettingsOpen) {
return const RouteInformation(location: '/settings');
}
return const RouteInformation(location: '/');
}
}
2.3 实现 RouterDelegate
class AppRouterDelegate extends RouterDelegate<AppRouteState>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRouteState> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
AppRouteState _routeState = const AppRouteState();
AppRouteState get routeState => _routeState;
void _updateState(AppRouteState newState) {
_routeState = newState;
notifyListeners(); // 通知 Router 重建
}
// 导航方法
void goToLogin() => _updateState(const AppRouteState(isLoggedIn: false));
void goToHome() => _updateState(const AppRouteState(isLoggedIn: true));
void goToProduct(String productId) => _updateState(
_routeState.copyWith(selectedProductId: productId),
);
void goToSettings() => _updateState(
_routeState.copyWith(isSettingsOpen: true),
);
void goBack() {
if (_routeState.isSettingsOpen) {
_updateState(_routeState.copyWith(isSettingsOpen: false));
} else if (_routeState.selectedProductId != null) {
_updateState(_routeState.copyWith(clearProduct: true));
}
}
@override
AppRouteState get currentConfiguration => _routeState;
@override
Future<void> setNewRoutePath(AppRouteState configuration) async {
// 外部路由变化(如浏览器 URL 改变)→ 更新内部状态
_routeState = configuration;
notifyListeners();
}
@override
Widget build(BuildContext context) {
// 根据状态声明式地构建页面栈
return Navigator(
key: navigatorKey,
pages: [
// 根页面(始终存在)
if (!_routeState.isLoggedIn)
MaterialPage(
key: const ValueKey('LoginPage'),
child: LoginPage(onLogin: goToHome),
),
if (_routeState.isLoggedIn)
MaterialPage(
key: const ValueKey('HomePage'),
child: HomePage(
onProductTap: goToProduct,
onSettingsTap: goToSettings,
onLogout: goToLogin,
),
),
// 条件页面(基于状态决定是否存在于栈中)
if (_routeState.isLoggedIn && _routeState.selectedProductId != null)
MaterialPage(
key: ValueKey('ProductPage-${_routeState.selectedProductId}'),
child: ProductDetailPage(
productId: _routeState.selectedProductId!,
),
),
if (_routeState.isLoggedIn && _routeState.isSettingsOpen)
const MaterialPage(
key: ValueKey('SettingsPage'),
child: SettingsPage(),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
goBack(); // 系统返回时同步更新状态
return true;
},
);
}
}
// 在 MaterialApp 中使用
void main() {
final routerDelegate = AppRouterDelegate();
final routeParser = AppRouteInformationParser();
runApp(
MaterialApp.router(
routerDelegate: routerDelegate,
routeInformationParser: routeParser,
),
);
}
三、Pages API:声明式页面栈的本质
Navigator 2.0 最重要的思想:页面栈是状态的函数,而不是命令操作的结果。
// Navigator 1.0(命令式):
// 状态:_showDetail = true
// 操作:Navigator.push(DetailPage()) ← 产生副作用
// Navigator 2.0(声明式):
// 状态:_showDetail = true
// 渲染:pages = [HomePage, if (_showDetail) DetailPage()] ← 纯函数
Navigator(
pages: [
const MaterialPage(child: HomePage()),
if (showDetail)
MaterialPage(
key: const ValueKey('detail'),
child: DetailPage(),
),
],
onPopPage: (route, result) {
// 用户 pop 时,更新状态而不是直接操作栈
setState(() => showDetail = false);
return route.didPop(result);
},
)
Page 的 key 很重要:Navigator 通过 key 来判断 Page 是否是"同一个页面",从而决定是否复用对应的 Route(保持动画状态、避免重建)。
四、go_router:Navigator 2.0 的简化封装
直接使用 Navigator 2.0 的原始 API 过于复杂。go_router 是目前最主流的 Navigator 2.0 封装库。
4.1 基础配置
// router.dart
final router = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
// 子路由
GoRoute(
path: 'product/:id', // 路径参数
builder: (context, state) {
final id = state.pathParameters['id']!;
return ProductDetailPage(productId: id);
},
),
],
),
GoRoute(
path: '/settings',
builder: (context, state) => const SettingsPage(),
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
],
// 错误页面
errorBuilder: (context, state) => ErrorPage(error: state.error),
);
// main.dart
void main() {
runApp(
MaterialApp.router(
routerConfig: router,
),
);
}
4.2 导航操作
// context.go:替换当前路由栈(不能返回)
context.go('/login');
// context.push:压栈(可以返回)
context.push('/product/123');
// context.pop:返回
context.pop();
context.pop(result); // 携带返回值
// context.replace:替换当前页面
context.replace('/home');
// 路径参数和查询参数
context.go('/product/123?from=home');
// 获取参数:state.pathParameters['id']
// state.uri.queryParameters['from']
// 携带额外数据(不出现在 URL 中)
context.push('/product/123', extra: {'product': productObject});
// 获取:state.extra as Map<String, dynamic>
4.3 路由守卫(认证重定向)
final router = GoRouter(
routes: [ /* ... */ ],
redirect: (BuildContext context, GoRouterState state) {
// 全局重定向:在每次路由变化时调用
final isLoggedIn = AuthService.instance.isLoggedIn;
final isLoginPage = state.matchedLocation == '/login';
if (!isLoggedIn && !isLoginPage) {
// 未登录 → 重定向到登录页,并保存原始目标路径
return '/login?redirect=${state.uri}';
}
if (isLoggedIn && isLoginPage) {
// 已登录 → 不能再访问登录页
final redirect = state.uri.queryParameters['redirect'];
return redirect ?? '/';
}
return null; // null = 不重定向,继续正常导航
},
// 响应式重定向:当指定 Listenable 变化时自动重新评估 redirect
refreshListenable: AuthService.instance, // AuthService 继承 ChangeNotifier
);
4.4 嵌套路由与 ShellRoute
ShellRoute 用于实现底部导航栏、侧边栏等持久 UI:
final router = GoRouter(
routes: [
ShellRoute(
// Shell Widget:持久的底部导航栏
builder: (context, state, child) {
return ScaffoldWithNavBar(child: child); // child 是当前子路由的内容
},
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
),
GoRoute(
path: '/explore',
builder: (context, state) => const ExplorePage(),
routes: [
GoRoute(
path: 'detail/:id',
builder: (context, state) => ExploreDetailPage(
id: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfilePage(),
),
],
),
// 不在 Shell 内的路由(全屏页面,无底部导航栏)
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
],
);
// 底部导航栏 Widget
class ScaffoldWithNavBar extends StatelessWidget {
final Widget child;
const ScaffoldWithNavBar({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
currentIndex: _calculateSelectedIndex(context),
onTap: (index) => _onItemTapped(index, context),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'),
BottomNavigationBarItem(icon: Icon(Icons.explore), label: '发现'),
BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'),
],
),
);
}
int _calculateSelectedIndex(BuildContext context) {
final location = GoRouterState.of(context).uri.toString();
if (location.startsWith('/explore')) return 1;
if (location.startsWith('/profile')) return 2;
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0: context.go('/'); break;
case 1: context.go('/explore'); break;
case 2: context.go('/profile'); break;
}
}
}
4.5 StatefulShellRoute:每个 Tab 独立导航栈
final router = GoRouter(
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavBar(navigationShell: navigationShell);
},
branches: [
StatefulShellBranch(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'detail/:id',
builder: (context, state) => DetailPage(id: state.pathParameters['id']!),
),
],
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => const ProfilePage(),
),
],
),
],
),
],
);
class ScaffoldWithNavBar extends StatelessWidget {
final StatefulNavigationShell navigationShell;
const ScaffoldWithNavBar({super.key, required this.navigationShell});
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell, // 每个 Tab 有独立的 Navigator
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex, // 双击 Tab 回到该 Branch 的根
),
items: const [/* ... */],
),
);
}
}
五、Deep Link 集成
5.1 Android 配置
<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" android:host="open" />
<data android:scheme="https" android:host="myapp.com" />
</intent-filter>
</activity>
5.2 iOS 配置
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:myapp.com</string>
</array>
5.3 go_router 自动处理 Deep Link
// go_router 会自动将 Deep Link URL 映射到对应的路由
// myapp://open/product/123 → 打开 ProductDetailPage(id: '123')
// https://myapp.com/product/123 → 同上
final router = GoRouter(
routes: [
GoRoute(
path: '/product/:id',
builder: (context, state) => ProductDetailPage(
productId: state.pathParameters['id']!,
),
),
],
);
六、Navigator 2.0 解决了 1.0 的哪些问题?
| 问题 | Navigator 1.0 | Navigator 2.0 |
|---|
| URL 同步 | ❌ 无法同步 | ✅ RouteInformationParser 双向绑定 |
| 浏览器前进/后退 | ❌ 无法处理 | ✅ setNewRoutePath 接收外部路由变化 |
| Deep Link | ⚠️ 需要手动处理(初始路由) | ✅ 自动解析 URL → 状态 → 页面栈 |
| 路由状态序列化 | ❌ 命令式操作无法序列化 | ✅ 状态即路由,可持久化 |
| 声明式页面栈 | ❌ 命令式 push/pop | ✅ pages 列表是状态的函数 |
| 学习成本 | ✅ 简单 | ⚠️ 复杂(go_router 封装后还好) |
七、实战:完整的 go_router 导航体系
// 认证状态
final authProvider = ChangeNotifierProvider<AuthNotifier>((ref) => AuthNotifier());
// go_router 配置
final routerProvider = Provider<GoRouter>((ref) {
final authNotifier = ref.watch(authProvider);
return GoRouter(
initialLocation: '/',
refreshListenable: authNotifier, // 认证状态变化时重新评估 redirect
redirect: (context, state) {
final isLoggedIn = authNotifier.isLoggedIn;
final isOnLoginPage = state.matchedLocation == '/login';
if (!isLoggedIn && !isOnLoginPage) return '/login';
if (isLoggedIn && isOnLoginPage) return '/';
return null;
},
routes: [
// 认证页面(无 Shell)
GoRoute(
path: '/login',
builder: (context, state) => const LoginPage(),
),
// 主应用(有 Shell)
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return MainScaffold(navigationShell: navigationShell);
},
branches: [
// Tab 1:首页
StatefulShellBranch(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomePage(),
routes: [
GoRoute(
path: 'product/:id',
builder: (context, state) => ProductDetailPage(
id: state.pathParameters['id']!,
),
),
],
),
],
),
// Tab 2:订单
StatefulShellBranch(
routes: [
GoRoute(
path: '/orders',
builder: (context, state) => const OrderListPage(),
routes: [
GoRoute(
path: ':orderId',
builder: (context, state) => OrderDetailPage(
orderId: state.pathParameters['orderId']!,
),
),
],
),
],
),
// Tab 3:我的
StatefulShellBranch(
routes: [
GoRoute(
path: '/profile',
builder: (context, state) => const ProfilePage(),
routes: [
GoRoute(
path: 'settings',
builder: (context, state) => const SettingsPage(),
),
GoRoute(
path: 'edit',
builder: (context, state) => const EditProfilePage(),
),
],
),
],
),
],
),
],
errorBuilder: (context, state) => ErrorPage(error: state.error),
);
});
// 在 Riverpod + go_router 组合中
void main() {
runApp(
ProviderScope(
child: Consumer(
builder: (context, ref, _) {
return MaterialApp.router(
routerConfig: ref.watch(routerProvider),
);
},
),
),
);
}
过关自检
Q1:Navigator 2.0 解决了 Navigator 1.0 的哪些核心问题?
见第六节对比表。核心问题:URL 同步(Web 场景下地址栏不更新)、浏览器前进后退无法映射到路由栈、Deep Link 处理困难、路由栈无法序列化(无法持久化应用状态)。Navigator 2.0 通过引入 RouteInformationParser(URL ↔ 状态)和 RouterDelegate(状态 → pages 列表)的三角架构来解决。
Q2:go_router 如何实现认证守卫和嵌套路由?
-
认证守卫:通过 redirect 回调在每次路由变化时检查认证状态,返回重定向目标路径或 null(不重定向)。配合 refreshListenable: authNotifier,认证状态变化时自动重新评估 redirect,实现登录/登出后的自动跳转。
-
嵌套路由:通过在 GoRoute 的 routes 列表中嵌套子路由实现层级关系。ShellRoute 提供持久 UI(如底部导航栏),内部的子路由切换不会重建 Shell。StatefulShellRoute.indexedStack 让每个 Tab 维护独立的导航栈,切换 Tab 时保留各自的页面历史。
小结
| 概念 | 要点 |
|---|
| Navigator 2.0 三角架构 | RouteInformationParser + RouterDelegate + Navigator |
| Pages API | 页面栈是状态的函数,非命令式操作 |
| go_router | Navigator 2.0 的简化封装,路径参数/守卫/Shell |
| context.go | 替换路由栈(不可返回),类似 pushReplacement |
| context.push | 压栈(可返回),类似 Navigator.push |
| redirect | 路由守卫,认证重定向 |
| refreshListenable | 状态变化时自动重新评估 redirect |
| ShellRoute | 持久 UI(底部导航栏),子路由共享 Shell |
| StatefulShellRoute | 每个 Tab 独立导航栈 |
| Deep Link | 平台配置 + go_router 自动 URL → 路由映射 |