本节摘要:页面流转有两代写法:Navigator 1.0 把界面当一叠卡片,压栈弹栈;Navigator 2.0 把界面当一张地图,每个页面一个地址,状态说了算。本节讲清两代的心智差别与各自的正确用途,用声明式路由把轻记账的全部页面收进一张表,并配置深度链接让系统入口直达账单详情。转场动画作为收尾,把第 3 章的动画工艺接到页面上。
Navigator 1.0 的心智是一叠卡片:push 把新页压上栈顶,pop 把栈顶弹掉,永远面对栈顶。它直白、好用,多数内部跳转至今仍这么写:
// 压栈:带着账单 id 去详情页 Navigator.of(context).pushNamed('/bill/$id'); // 弹栈并回传结果:第 3 章的记账面板就是这么交回数据的 Navigator.of(context).pop({'category': _category, 'amount': amount}); // 等待回传:记账页拿到结果后刷新 final result = await Navigator.of(context).pushNamed<Map>('/input'); if (result != null) debugPrint('记下一笔:$result');
push 与 pop 成对出现,返回值机制天然支持"子页面做完事交回结果"——记账面板、编辑对话框这类"去了就要回来"的流程,栈模型最顺手。
但栈模型有三个解不开的死结。其一,状态与界面脱节:Android 系统回收了后台页面,用户点通知栏回来,栈里是什么全凭系统心情,代码无从知晓也无力干预。其二,系统级入口进不来:用户从桌面长按图标直奔"扫码",系统只会启动应用默认首页,"直达某页"没有表达方式。其三,Web 端地址栏失灵:浏览器的前进后退与地址栏对应不了栈里的内容。三个死结的共同病根:界面由"命令序列"(push 过几次)决定,而不是由"当前状态"决定。

Navigator 2.0 的翻转在于控制权倒置:不再是代码命令"跳到哪",而是应用状态声明"现在该显示什么",路由层负责把状态对齐成界面。它由三个部件组成:解析器(把外部地址翻成内部类型)、路由状态(当前页面栈的描述)、以及根据状态装配页面的出口。
从零拼这三个部件是理解机制的好练习,但业务工程通常直接用封装库(如 GoRouter)——它把样板收干净,保留了声明式的全部能力。轻记账的整张路由表:
final router = GoRouter( initialLocation: '/today', routes: [ GoRoute( path: '/today', builder: (context, state) => const TodayPage(), routes: [ GoRoute( path: 'input', // 叠在今日页之上的记账面板 builder: (context, state) => const BillInputPage(), ), ], ), GoRoute( path: '/stats', builder: (context, state) => const StatsPage(), ), GoRoute( path: '/bill/:id', // 参数化地址:账单详情 builder: (context, state) { final id = state.pathParameters['id']!; return BillDetailPage(billId: id); }, ), ], ); MaterialApp.router(routerConfig: router);
页内跳转从此有了两种表达。命令式仍是顺手的选择(context.push('/today/input'));声明式则在"界面应当是什么"的层面操作(context.go('/stats')),适合登录态切换、注销清栈这类"整个界面格局变了"的场景。两者的分界与状态管理的"意图"对齐:按钮点击是意图,用 push;登录状态变化是状态,用 go。
声明式路由落地后,深度链接几乎是免费赠品:外部传来一个地址,路由表直接解析装配。要让操作系统能把链接递给应用,移动端还要在宿主壳里登记关联域名(Android 的 intent filter 与 iOS 的关联域名各配一处,第 7 章打开宿主壳时细说),Flutter 侧只需接住初始地址并开启路径复用:
MaterialApp.router( routerConfig: router, // GoRouter 默认处理初始路径与浏览器地址变化;移动端深度链接到达后 // 会以初始路径或路径变更事件进入同一套路由表,无需另写解析代码。 );
轻记账的真实场景走一遍:账单详情页给了一个分享链接,收件人点开,操作系统唤起应用并把地址交给路由表,BillDetailPage 按 id 参数装载数据——用户一步直达,中间的首页列表从未出现。这个体验在栈模型时代要手写一整套"冷启动分支逻辑",在地图模型里就是路由表里的一行参数化路径。Web 端同一段路由表还顺带管好了地址栏与浏览器前进后退——一份路由表,三端受益。
页面转场就是页面级的动画,两代路由都能自定义。栈模型下替换默认转场只需 PageRouteBuilder;声明式路由下用 CustomTransitionPage:
GoRoute( path: '/bill/:id', pageBuilder: (context, state) => CustomTransitionPage( key: state.pageKey, child: BillDetailPage(billId: state.pathParameters['id']!), transitionsBuilder: (context, animation, _, child) { final curved = CurvedAnimation(parent: animation, curve: Curves.easeOutCubic); return FadeTransition( opacity: curved, child: SlideTransition( position: Tween(begin: const Offset(0, 0.04), end: Offset.zero).animate(curved), child: child, ), ); }, ), )
转场的分寸感沿用第 3 章的结论:默认的平台转场(Material 缩放、Cupertino 右缘推入)已经过千锤百炼,自定义转场留给品牌关键页,别整页皆动。顺带一提第 3.4 节的分端转场主题在 GoRouter 下同样生效——路由层不抢主题层的活。
到此,轻记账的状态、存储、流转全部就位。下一章打开数据的大门:网络请求、报文模型与云端同步。