本专题为软件平台路由规则设计(含移动端、Web、电脑、手表与手环等智能穿戴)。本文对应「声明式路由与 Navigator 2.0」阶段,介绍状态驱动路由栈、Flutter Navigator 2.0 三件套、go_router 用法,以及 iOS、Android、HarmonyOS、MacOS、WinOS、WebApp、ReactNative、WatchOS 等平台的声明式/状态驱动思路,含多端代码示例与流程图。多端对照见 08-软件体系与多平台路由对照。
一、概念与定位
1.1 命令式 vs 声明式路由
- 命令式:开发者显式调用「push / pop / replace」等 API,直接操作导航栈;栈状态由多次调用累积而成,难以从「当前状态」反推完整栈。
- 声明式:用一个状态(如「当前要展示的页面列表」)描述整个路由栈,框架根据该状态推导出应对栈做的增删,从而与「状态驱动 UI」的范式一致,便于深链接、多 Tab、复杂返回栈和 Web URL 同步。
Navigator 2.0 即 Flutter 提供的声明式导航 API:由应用维护 List<Page>(或等价状态),Navigator 根据该列表与旧列表的差异自动执行 push/pop/replace,实现「状态即路由栈」。
1.2 知识结构(思维导图)
mindmap
root((声明式路由 / Navigator 2.0))
核心思想
状态描述栈
框架推导操作
URL 与状态同步
Flutter 三件套
Router
RouteInformationParser
RouterDelegate
go_router
path / query / redirect
refreshListenable
其他平台 九端
iOS Android HarmonyOS
MacOS WinOS WebApp
ReactNative WatchOS
应用场景
深链接
多 Tab / 底部栏
Web 同构
二、Flutter Navigator 2.0 架构流程图
flowchart TB
subgraph 外部
A[系统 / 浏览器 URL]
B[RouteInformation]
end
subgraph Flutter 层
C[Router]
D[RouteInformationParser]
E[RouterDelegate]
F[Navigator + pages]
end
subgraph 状态
G[AppRouteState / List of Page]
end
A --> B --> C
C --> D
D --> G
G --> E
E --> F
F --> H[当前页面树]
H --> I[用户操作 / 返回]
I --> G
三、Flutter 核心组件与数据流(泳道图)
flowchart LR
subgraph 系统
S1[RouteInformation]
end
subgraph Parser
P1[parseRouteInformation]
P2[restoreRouteInformation]
end
subgraph Delegate
D1[setNewRoutePath]
D2[build → Navigator]
D3[popRoute]
end
subgraph 状态
ST[pages / routeState]
end
S1 --> P1 --> ST
ST --> D2
D1 --> ST
D3 --> ST
P2 --> S1
四、Flutter 代码示例:RouterDelegate + RouteInformationParser
4.1 路由状态
// route_state.dart
class AppRouteState {
final List<Page<dynamic>> pages;
const AppRouteState({this.pages = const []});
AppRouteState push(Page<dynamic> page) {
return AppRouteState(pages: [...pages, page]);
}
AppRouteState pop() {
if (pages.length <= 1) return this;
return AppRouteState(pages: pages.sublist(0, pages.length - 1));
}
AppRouteState replaceAll(List<Page<dynamic>> newPages) {
return AppRouteState(pages: newPages);
}
}
4.2 RouteInformationParser:URL ↔ 状态
// app_route_parser.dart
class AppRouteInformationParser extends RouteInformationParser<AppRouteState> {
@override
Future<AppRouteState> parseRouteInformation(RouteInformation info) async {
final uri = Uri.parse(info.location ?? '/');
final path = uri.path;
final query = uri.queryParameters;
final pages = <Page<dynamic>>[
MaterialPage(child: HomePage(), name: '/'),
];
if (path.startsWith('/detail/')) {
final id = path.split('/').last;
pages.add(MaterialPage(
child: DetailPage(id: id, extra: query['from']),
name: '/detail/$id',
));
} else if (path == '/list') {
pages.add(MaterialPage(child: ListPage(), name: '/list'));
}
return AppRouteState(pages: pages);
}
@override
RouteInformation? restoreRouteInformation(AppRouteState state) {
if (state.pages.isEmpty) return null;
final last = state.pages.last;
return RouteInformation(location: last.name);
}
}
4.3 RouterDelegate:状态 → Navigator
// app_router_delegate.dart
class AppRouterDelegate extends RouterDelegate<AppRouteState>
with PopNavigatorRouterDelegateMixin<AppRouteState>, ChangeNotifier {
AppRouterDelegate() : _state = AppRouteState();
AppRouteState _state;
AppRouteState get state => _state;
@override
GlobalKey<NavigatorState> get navigatorKey => _navigatorKey;
final _navigatorKey = GlobalKey<NavigatorState>();
void push(Page<dynamic> page) {
_state = _state.push(page);
notifyListeners();
}
void pop() {
_state = _state.pop();
notifyListeners();
}
@override
AppRouteState get currentConfiguration => _state;
@override
Future<void> setNewRoutePath(AppRouteState configuration) async {
_state = configuration;
notifyListeners();
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: _state.pages,
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
pop();
return true;
},
);
}
}
4.4 MaterialApp 接入
// main.dart
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
final _delegate = AppRouterDelegate();
final _parser = AppRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: _delegate,
routeInformationParser: _parser,
);
}
}
4.5 业务侧「声明式」跳转
不直接操作栈,而是通过更新状态(由 Delegate 暴露的方法)驱动:
// 获取 delegate 可通过 InheritedWidget 或全局
routerDelegate.push(MaterialPage(
child: DetailPage(id: '123'),
name: '/detail/123',
));
五、go_router 使用示例(推荐实践)
go_router 在 Navigator 2.0 之上封装了 path/query、redirect、refreshListenable,更贴近 Web 路由与深链接。
5.1 基本配置(Dart)
// go_router_config.dart
final goRouter = GoRouter(
initialLocation: '/',
routes: [
GoRoute(path: '/', builder: (_, __) => HomePage()),
GoRoute(
path: '/detail/:id',
builder: (_, state) => DetailPage(
id: state.pathParameters['id']!,
from: state.uri.queryParameters['from'],
),
),
GoRoute(path: '/list', builder: (_, __) => ListPage()),
],
redirect: (context, state) {
// 未登录时重定向到登录页
if (!AuthManager.isLoggedIn && state.matchedLocation != '/login') {
return '/login';
}
return null;
},
refreshListenable: AuthNotifier(), // 登录态变化时重新执行 redirect
);
5.2 跳转与传参(Dart)
// 跳转
context.go('/detail/123');
context.go('/detail/123?from=home');
context.push('/list');
// 带 extra
context.push('/detail/123', extra: MyData());
// 在 builder 中: state.extra as MyData
5.3 与深链接统一
系统传入的 RouteInformation(如 https://example.com/detail/1)经 go_router 解析为 path /detail/1 与 query,与 App 内 context.go('/detail/1') 行为一致,实现「一条 URL、一套状态」。
六、状态差异与栈操作推导(伪代码)
声明式路由的核心:根据「目标页面列表」与「当前栈」的差异,推导出应执行的 push/pop/replace。
函数 computeStackDiff(currentPages, targetPages):
// currentPages、targetPages 均为页面标识列表(如 path 或 name)
// 返回待执行操作序列: [{ op: "push", page }, { op: "pop" }, { op: "replace", page }]
1. 找到最长公共前缀长度 prefixLen(从前往后逐项比较)
2. 若 targetPages.length > currentPages.length:
对 i = prefixLen 到 targetPages.length-1,生成 push(targetPages[i])
3. 若 targetPages.length < currentPages.length:
生成 (currentPages.length - targetPages.length) 次 pop
4. 若 prefixLen < 两者最小值 且 某项不同:
可生成 replace(targetPages[prefixLen]) 再 push 后续
5. 返回操作序列
七、iOS / Android 的声明式思路简述
7.1 iOS:用状态描述「要展示的 VC 栈」
不直接 push/pop,而是维护一个「要展示的 VC 列表」状态,根据状态与当前栈的差异做 push/pop:
// 状态
struct AppRouteState {
var stack: [RouteItem] // [.home, .detail(id: "123")]
}
// 根据 state 与当前 navigationController.viewControllers 的差异,执行 push/pop
func apply(state: AppRouteState) {
let current = nav.viewControllers.map { ... }
let target = state.stack.map { createVC($0) }
// 计算 diff,执行 setViewControllers 或 push/pop
}
7.2 Android:用状态描述「Activity 栈或 Fragment 栈」
同理,用「要展示的页面列表」作为唯一真实来源,通过 NavController + 自定义 BackStack 或 Single Activity + Fragment 栈的 state 驱动:
// 例如 Jetpack Navigation 的 NavBackStackEntry 列表即「状态」
// 通过 NavController.navigate(uri) 或 NavController.popBackStack() 更新栈
// 深链接时:NavController.setGraph() 或 handleDeepLink(intent) 设置与 URL 一致的状态
八、声明式路由与 URL 同步时序图
sequenceDiagram
participant U as 用户/系统
participant P as Parser
participant State as RouteState
participant D as Delegate
participant Nav as Navigator
U->>P: RouteInformation (URL)
P->>State: parseRouteInformation → new state
State->>D: setNewRoutePath(state)
D->>D: notifyListeners
D->>Nav: build(pages: state.pages)
Nav->>Nav: 对比旧 pages,执行 push/pop/replace
Note over U,Nav: 用户点击返回
Nav->>D: onPopPage
D->>State: pop()
D->>P: restoreRouteInformation(state)
P-->>U: RouteInformation (更新 URL)
九、各平台声明式/状态驱动路由
| 平台 | 声明式/状态驱动体现 |
|---|---|
| Flutter | Navigator 2.0、go_router(state → pages) |
| WebApp | React Router / Vue Router(URL/state 即路由) |
| ReactNative | React Navigation(state 驱动栈) |
| iOS / MacOS | 可自建「目标 VC 列表」状态,diff 后 setViewControllers |
| Android | Jetpack Navigation、Single Activity + Fragment 状态 |
| HarmonyOS | 页面栈与 state 对应,可声明式更新栈 |
| WinOS | 框架内 Frame 与 state 绑定 |
| WatchOS | 层级少,可简单 state 驱动界面栈 |
十、小结
- 声明式路由:用「状态」描述整棵路由栈,由框架根据状态差异推导 push/pop/replace,便于与 URL、深链接、多 Tab 一致。
- Flutter Navigator 2.0:
Router+RouteInformationParser(URL ↔ 状态)+RouterDelegate(状态 → Navigator.pages),实现状态驱动栈;go_router 在此基础上提供 path/query、redirect、refreshListenable,推荐在新项目或需要深链接时使用。 - iOS/Android:可借鉴「用状态描述栈 + 根据状态 diff 更新 VC/Activity/Fragment 栈」的思路,与 Flutter 声明式模型对齐。
十一、参考文献
- Flutter. Understanding Navigator 2.0.
- Flutter. go_router.
- 《01-软件平台路由规则设计-总纲》§2.2、§6.3.