Flutter路由与导航全攻略:从Navigator到go_router
发布时间:2026/10/9 10:48:02 锦皓数字建站

1. 为什么导航与路由是Flutter绕不开的第一道坎接触Flutter的第一周绝大多数人都会有同感写静态页面还算顺畅一旦涉及页面跳转、参数传递、返回刷新这些“日常操作”就开始被各种概念卡住——Navigator、Route、MaterialPageRoute、命名路由、onGenerateRoute……这些名词堆在一起看起来只是“跳转页面”四个字实际用起来却各有门道。我自己带过不少从Android、iOS、Vue转过来的新人发现一个规律凡是能把导航与路由梳理清楚的人后面学状态管理、组件通信、模块化拆分都会快很多。反过来说这部分一直停留在“能跑就行”状态的人写出来的Flutter应用往往页面耦合严重、改一处崩三处。导航与路由不只是“点按钮换页面”这么简单它本质上是整个App页面组织、状态传递、模块解耦的地基。这篇内容适合两类人一是刚入门Flutter、正在被Navigator.push和命名路由搞得一头雾水的初学者二是已经写了一段时间Flutter、想从“会跳转”进阶到“设计好页面导航架构”的中级开发者。我会从最基础的Navigator 1.0讲起再聊到声明式路由的Navigator 2.0最后结合实际项目经验把参数传递、路由守卫、生命周期那些坑一个个摊开说清楚。先说一个最容易踩的认知误区很多人刚学Flutter时看到Navigator.push觉得这玩意儿跟startActivity没啥区别看到命名路由又觉得这跟Vue Router差不多。其实Flutter的路由体系要比传统移动端或者Web端Router复杂得多它既是“页面栈管理器”又是“状态恢复机制”还是“深链接入口”。如果不理解这层设计逻辑后面遇到“为什么页面重建了”“为什么返回时数据丢了”“为什么热重启后路由栈错乱”这类问题基本无从下手。2. Navigator 1.0命令式路由的基石2.1 从MaterialPageRoute说起Flutter里最基础、最常用的跳转方式就是Navigator.push配合MaterialPageRoute。一段最朴素的代码长这样Navigator.push( context, MaterialPageRoute( builder: (context) const DetailPage(), ), );这段代码背后的运行逻辑我会拆成三步来说清楚。第一步MaterialPageRoute创建了一个路由对象。这个对象描述了“新页面长什么样”通过builder构建页面Widget、“进入动画是什么”Android上默认是自下而上的缩放淡入iOS上默认是右侧滑入、“页面被系统回收时如何处理”等一系列信息。换句话说Route是页面的一个描述元数据而不是页面本身。第二步Navigator把这个Route压入自己的管理栈。Navigator内部维护了一个_History栈栈里的每个元素对应一个页面。push操作就是往栈顶添加新路由pop操作就是从栈顶移除路由。这也是为什么页面跳转天然支持“返回”——栈这种数据结构天生就适合表达“从哪来、回哪去”。第三步Navigator根据栈的状态更新界面。栈顶的Route会生成对应的页面Widget显示出来新路由进入时旧路由并没有被销毁只是被暂停了或者说移出了焦点。我习惯用一个类比帮助理解Navigator就像一个放映员手里捏着一叠幻灯片每次只让你看最上面那张但下面的片子都还在。这就是为什么从详情页返回时列表页还保持着原来的滚动位置和状态。理解了这三步你就能明白为什么MaterialPageRoute的builder回调里拿到的context不是页面真正的BuildContext而是一个包含路由信息的上下文。很多初学者在这里踩坑在builder里用context去拿MediaQuery或者Theme没问题但如果用context去Navigator.push就可能报警告甚至报错——因为这个context还没有准备好执行导航操作。2.2 命名路由与路由表是方便也是枷锁用MaterialPageRoute直接push有一个明显的问题如果页面跳转关系很多代码里到处都充斥Navigator.push(context, MaterialPageRoute(...))这种样板代码而且页面跳转的“路径”被硬编码在业务逻辑里。为了解决这个问题Flutter提供了命名路由机制。命名路由的思路很直观给每个页面起一个字符串名字然后在MaterialApp里注册一个路由表跳转时只用名字MaterialApp( routes: { /: (context) const HomePage(), /detail: (context) const DetailPage(), /settings: (context) const SettingsPage(), }, );Navigator.pushNamed(context, /detail);看起来简洁了不少但这里有一个很多教程没提透的坑命名路由不支持直接传参至少官方静态注册方式是这样。你可能会想跳转详情页总得带个ID吧于是很多人会这样写Navigator.pushNamed(context, /detail, arguments: {id: 123});然后在DetailPage的build方法里通过ModalRoute.of(context).settings.arguments去取参数。这样写当然能跑但问题也随之而来参数字段没有类型约束全都变成了Map的key-value一旦页面多了、参数多了极易出现拼写错误而且IDE完全无法帮你检查。我见过不止一个项目就因为参数名从userName改成username漏改了一个页面导致线上偶发崩溃。所以我对命名路由的态度一直是有保留的。它在小型Demo、页面结构简单、且不需要带复杂参数的场景下确实省事但在中大型项目里我反而更推荐显式传参的MaterialPageRoute方式或者在命名路由上套一层类型安全的包装。后面讲go_router的时候会再展开。2.3 返回机制与数据回传说完去路说归途。Navigator.push返回的是一个Future这个设计很多人刚开始不理解直到需要“从下一个页面带回结果”时才发现它的妙处final result await Navigator.pushString( context, MaterialPageRoute( builder: (context) const SelectPage(), ), );在SelectPage里通过Navigator.pop(context, 选择了苹果)就能把字符串返回给上一个页面。整个过程和异步函数调用非常像——push像发起调用pop像是return而await则帮你接住返回值。这里要注意几点都是实战中容易出问题的第一pop时如果带了结果而这个结果又被另一个pop覆盖了会直接报错。比如你在页面上同时注册了系统返回键的监听和自定义返回按钮两次pop就会造成“返回一次却关掉两层页面”的混乱。解决思路是要么只留一个返回入口要么在系统返回时先判断Navigator.canPop()再决定是否执行pop。第二await Navigator.push的回调时机。如果你在SelectPage里用Navigator.pop(context, result)返回后紧接着在await之后的代码里访问上一个页面的状态可能偶发遇到context已被销毁的报错。这不是路由的问题而是异步时序页面生命周期的叠加效应。稳妥的做法是在await之后先判断mounted再继续操作。第三pop不传参数的情况。如果被弹出的页面没有显式pop值比如用户按了手机物理返回键await拿到的就是null。所以接收方必须对null做处理不能默认“只要返回了就有结果”。3. 参数传递的艺术与实操代码3.1 简单的值传递直接又直观先看最直接最简单的一种方式适合参数不多、结构不复杂的场景直接通过构造方法传参class DetailPage extends StatelessWidget { final int articleId; const DetailPage({super.key, required this.articleId}); }然后跳转Navigator.push( context, MaterialPageRoute( builder: (context) DetailPage(articleId: 42), ), );这种写法的最大优势就是类型安全。articleId是int就是int编译期就能发现传错类型的低级错误不会运行时黑屏。而且在IDE里跳转、查询有哪些参数都是直接可见的新人接手也很容易看懂。缺点也是显而易见的跟命名路由完全相反它要求页面跳转前必须先import目标页面文件然后在业务代码里显式构建页面对象。对于页面多、业务膨胀的项目来说各处都出现import xxx_page.dart会让模块之间的依赖关系变得纠缠不清架构评审时经常被吐槽“上层业务直接依赖了底层页面详情”。3.2 用arguments传递灵活但要用好如果你坚持用命名路由那arguments参数就必须用得规范。官方支持两种写法第一种是Navigator.pushNamed(context, /detail, arguments: {id: 42})。这种写成Map的我是真不推荐。你想想看这个页面可能被三个地方跳转每个地方塞的key都可能不同你根本不知道这个页面真正需要哪些key只能去翻实现。第二种写法稍微好一点——传一个自定义对象class DetailArguments { final int id; final String title; const DetailArguments({required this.id, required this.title}); }跳转时Navigator.pushNamed( context, /detail, arguments: DetailArguments(id: 42, title: Flutter路由深入), );在DetailPage中取参final args ModalRoute.of(context)?.settings.arguments as DetailArguments?;这种写法保障了参数结构而且后面要追加参数时只需修改DetailArguments类所有调用方的编译错误就能帮你自动排查出来。用自定义参数对象替代裸Map这是命名路由场景下性价比最高的优化。唯一的隐患是ModalRoute.of(context)可能返回null理论上不会但做空安全兜底总没坏处以及如果这个页面同时支持两种不同入口传参你得留意参数的null兼容。3.3 返回时带数据刷新列表经典翻车现场“从详情页返回后刷新列表”是我见过翻车最多的高频需求。常见错误做法是在详情页里pop回列表页后列表页在initState里重新请求数据。结果发现列表页根本没有重新走initState数据纹丝不动。原因前面已经讲过了Navigator.pop只是把栈顶页面移除下面的页面对象并没有重新创建initState自然不会再次执行。正确方案是在await之后触发刷新。比如Futurevoid _openDetail(int id) async { final changed await Navigator.pushbool( context, MaterialPageRoute( builder: (context) DetailPage(articleId: id), ), ); if (changed true) { // 重新拉取数据或更新本地状态 _fetchData(); } }然后详情页在需要“让上一个页面知道数据有变”的地方pop时带上true。还有一种更“响应式”的做法是用状态管理库比如Provider、Riverpod去驱动页面刷新。比如在详情页编辑完昵称直接修改一个共享的状态对象列表页因为监听了该状态而自动重建。这种方案的好处是彻底摆脱了“返回后手动通知”的链路坏处是要求你从一开始就把状态设计好否则容易变成把所有东西都塞进全局Store的脏架构。4. 路由守卫像Vue Router一样拦截导航4.1 为什么需要路由守卫接触Vue的开发者都知道Vue Router有全局前置守卫beforeEach、路由独享守卫、组件内守卫核心场景就是登录鉴权用户未登录时访问需要登录的页面就强制跳转到登录页。Flutter官方并没有提供一个跟Vue Router的beforeEach完全等价的“路由守卫API”——如果你去查文档会发现根本不存在Navigator.beforeEach这样的方法。但这个需求在真实项目中又非常常见所以大家用的都是变通方案。理解了底层机制实现起来其实不复杂甚至比Vue的路由守卫还灵活。4.2 用onGenerateRoute做统一拦截最常用的方案是自定义onGenerateRoute。当你没有在routes表中命中路由时Flutter会回调onGenerateRoute由你决定生成什么样的Route。利用这个时机做路由拦截是非常自然的MaterialApp( onGenerateRoute: (settings) { final needAuth settings.name /profile || settings.name /orders; if (needAuth !AuthManager.isLoggedIn) { return MaterialPageRoute( builder: (context) const LoginPage(), settings: settings, ); } return MaterialPageRoute( builder: (context) _buildPageByName(settings.name), ); }, );这样写的好处是所有导航统一收口。无论业务代码中是pushNamed、push还是深链接触发的路由走到onGenerateRoute时都能做一次统一的逻辑判断。登录状态、权限验证、页面参数预处理都可以在这个环节完成。有一点需要提醒onGenerateRoute里通常不要再调用Navigator去push跳转因为这时Navigator正在处理当前路由请求再push会导致路由栈操作嵌套容易引发断言错误。正确做法是像上面的示例一样直接返回一个Route对象让Navigator自己处理跳转而不是在回调里去“手动跳转”。4.3 用全局导航观察者实现后置守卫还有一种更细粒度的方案实现NavigatorObserver在didPush、didPop、didRemove等回调中监听每一次导航动作。这种方案适合做一些“路由日志埋点”“页面访问统计”或者统一处理页面返回时的刷新逻辑。class RouteObserver extends NavigatorObserver { override void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); debugPrint(新页面进入: ${route.settings.name}); // 可以在这里做页面曝光埋点 } override void didPop(Route route, Route? previousRoute) { super.didPop(route, previousRoute); debugPrint(页面退出: ${route.settings.name}); } }在MaterialApp里注册MaterialApp( navigatorObservers: [RouteObserver()], );需要注意NavigatorObserver只是“观察者”它决定不了“能否跳转”——如果你想拦截跳转而不只是记录就需要配合PopScope或者onGenerateRoute来实现。很多团队的实际方案是鉴权用onGenerateRoute埋点和统计用NavigatorObserver两者各司其职。4.4 单个页面的离开拦截PopScope很多时候我们不需要全局守卫只想在某个页面拦截返回操作比如编辑页有未保存内容时用户点返回要弹出确认框。老版本写法是WillPopScope现在已经废弃了要用PopScope。PopScope( canPop: _isSaved, onPopInvokedWithResult: (didPop, result) { if (!didPop) { // 弹窗提示用户是否确认离开 _showExitDialog(); } }, child: Scaffold(...), );这里的逻辑细节值得说清楚canPop为false时系统返回手势返回、物理返回键、AppBar默认返回按钮都会先触发onPopInvokedWithResult回调但页面不会真正退出你在回调里弹窗用户确认后再手动Navigator.pop。如果canPop为true则页面正常退出回调里的didPop会是true。这个API的名字很直观但也容易误读onPopInvokedWithResult并不是“要不要允许pop”的决策点而是“pop行为发生之后的通知”。如果不理解这一点你可能会试图在回调里调用Navigator.pop结果又触发了新一轮拦截逻辑造成递归。正确的流程我一般这样写Futurevoid _confirmExit() async { final shouldExit await showDialogbool( context: context, builder: (context) AlertDialog( title: const Text(内容尚未保存), actions: [ TextButton( onPressed: () Navigator.pop(context, false), child: const Text(继续编辑), ), TextButton( onPressed: () Navigator.pop(context, true), child: const Text(放弃修改), ), ], ), ); if (shouldExit true) { Navigator.pop(context); } }5. 页面生命周期与Navigator的联动5.1 路由切换时页面方法调用顺序Flutter页面的生命周期并不像Android那样有onPause/onStop的细粒度回调但这不代表没有生命周期概念。当从A页push到B页时A页会经历如下变化A页的build不会重新执行但A页状态会进入inactive不活跃状态不过它依然挂在元素树里。B页执行initState、build变成当前焦点页面。从B返回A时B页被销毁dispose被调用。A页重新变为活跃状态——但不会重新执行initState因为A的State对象还在。这个机制对于理解“哪些状态可以保留哪些必须重建”很关键。A页的ScrollController、TabController、表单内容等在跳转B页后其实都还保留着所以返回时列表位置不丢、输入框内容不丢。但是如果你依赖build来“刷新数据”那就麻烦了——因为返回A时可能只触发一次didChangeDependencies或者build而如果你在这里发起网络请求会导致每次从任何页面返回都会重新请求这在列表页是非常严重的性能问题。5.2 路由栈与热重载、内存回收Flutter的热重载Hot Reload是一个开发利器但跟路由栈配合时有个经典坑当你已经push到第3层页面此时执行热重载Flutter会尽量保持当前路由栈结构。但如果你在热重载前修改了路由表删除了某个页面构造函数参数栈里的旧页面可能无法重建轻则报错、重则卡死在白屏。遇到这种情况我的习惯是改路由相关代码时先Navigator.popToFirst回到根页面再热重载或者干脆热重启Hot Restart。这看起来是很笨的办法实际上却是最节省时间的方式。还有一个容易被忽略的点Navigator的栈是会一直持有页面的。如果你在push了数量极多的页面后从不pop比如某些循环跳转场景内存占用会持续上涨。虽然现代手机内存够大但页面里的图片、控制器、流订阅都会累积。审计自己的项目时留意一下是否有“层层push却没有返回入口”的页面链路。我见过一个外包项目用户疯狂滑动Feed流进入详情再进入详情最后App直接被杀原因就是这个。5.3 嵌套Navigator底部Tab与全局页面的冲突底部导航栏BottomNavigationBar和业务页面栈的关系是路由设计里的另一个大坑。很多初学者直接把TabBarView和Navigator.push混用结果发现从Tab1的子页面跳转到详情页后点击底部Tab切换详情页没消失界面状态完全错乱了。这背后的原因是MaterialApp默认有一个全局Navigator但每个Tab里的页面如果也维护了自己的子Navigator就会形成嵌套导航栈。返回时子Navigator先出栈还是全局Navigator出栈取决于当前焦点在哪个栈里。为了避免混乱常见的架构方案有两种方案一一个Navigator用页面栈模拟Tab。这种方案下底部Tab切换本质上也是push/pop页面。因为所有页面都在同一个栈里返回行为好预测。缺点是Tab切换时没有“保留每个Tab独立状态”的原生便利你得自己保存状态。方案二嵌套Navigator每个Tab维护独立栈。这种方案每个Tab都有自己的完整页面栈切Tab时状态天然保留。缺点是全局跳转比如从Tab1的详情页跳到一个全屏Modal页面得仔细选择到底用哪个Navigator。用错了页面的返回按钮就消失了或者行为诡异。我自己在项目里比较倾向“状态管理负责Tab状态、全局Navigator只负责跨Tab跳转”的混合模式。具体讲起来又是一篇长文这里先提个醒在设计底部导航时就要考虑清楚你的返回键在Tab切换后的行为究竟是“退出App”还是“回到上一个Tab”这决定了你的嵌套结构怎么搭。6. Navigator 2.0与声明式路由大项目的地基6.1 命令式路由的边界在哪Navigator 1.0这套命令式API在小中型项目里完全够用但一旦业务变大痛点就很明显了路由栈状态是“隐式”的散落在各个业务代码的push/pop调用里出了问题不好追踪。页面跳转逻辑和业务逻辑耦合测试时不太容易模拟特定路由状态。对深链接Deep Link、Web端URL同步这些能力的支持比较弱。Navigator 2.0就是冲着这些问题来的。它的核心思路是把路由变成“状态”而非“命令”——你声明“当前应用处于什么状态”框架负责把状态翻译成界面。这里不需要绕太多术语你只需要理解两个核心类就够了RouteInformationParser负责把RouteInformation比如URL字符串解析成内部路由状态数据。RouterDelegate负责根据路由状态数据构建Navigator的页面栈。比如在Web端如果你在浏览器地址栏输入https://example.com/items/42RouteInformationParser需要告诉你“当前想打开ID为42的商品详情页”RouterDelegate再根据这个意图去更新界面。6.2 什么时候该上Navigator 2.0需要老实说Navigator 2.0的API设计并不讨喜。刚接触的时候你会被它绕晕写一个最简单的“跳转到详情页”你需要同时改造RouterDelegate、RouteInformationParser还要处理BackButtonDispatcher代码量比命令式方式多出不少。所以我个人的建议很明确如果你的项目只是普通的App、没有深链接需求、不需要地址栏同步那就继续用Navigator 1.0别盲目跟风升级。“够用就好”在这个场景下不是一句敷衍而是基于学习和维护成本的判断。什么时候需要认真考虑Navigator 2.0或者基于它封装的库呢我总结了几类场景你的App需要在Web端跑并且希望浏览器地址栏内容和页面状态同步——比如用户复制URL发给别人对方打开直接落地到对应页面。你需要支持复杂的深链接规则比如收到推送消息跳转到特定页面、然后还能正常返回。你的页面栈状态被外部系统驱动比如物联网大屏、多端联动需要把“路由意图”和“路由界面”解耦。前两类场景在电商、内容社区类App里很常见。第三类相对小众但如果你在做桌面端或大屏项目Navigator 2.0的声明式思维会让你省很多事。6.3 go_router更友好的声明式路由选择Navigator 2.0的原生API学习曲线比较陡所以社区涌现了一批封装库其中最出名的就是go_router。它对声明式路由做了非常友好的封装让开发者可以用接近Vue Router的方式配置路由表GoRouter( initialLocation: /, routes: [ GoRoute( path: /, builder: (context, state) const HomePage(), ), GoRoute( path: /detail/:id, builder: (context, state) DetailPage( articleId: int.parse(state.pathParameters[id]!), ), ), ], );跳转时context.go(/detail/42);go_router不仅处理了参数解析还内置了redirect机制可以当路由守卫用GoRouter( redirect: (context, state) { final loggedIn AuthManager.isLoggedIn; final goingToLogin state.matchedLocation /login; if (!loggedIn !goingToLogin) { return /login; } return null; }, )这段代码的含义是未登录用户访问任何需要登录的页面自动重定向到登录页。逻辑上跟Vue Router的beforeEach非常接近只是写法不同而已。go_router在官方文档中被持续维护也是Flutter团队推荐的第三方路由方案之一。如果你的团队人手充足、项目处在快速增长期我建议直接用go_router来搭导航层它比裸写Navigator 2.0省下至少三分之一的心力。当然如果你喜欢完全掌控底层行为、不想要库依赖那用官方方案也没问题只是要准备好写更多样板代码。7. 常见问题排查与避坑实录7.1 页面被build了多次用Navigator.push跳转新页面后发现新页面build被调用了两次甚至多次。先别急改代码这是正常的。原因在于路由页面进入时框架会先以“待定尺寸”build一次随后动画开始、尺寸确定后再build一次。只要build是纯函数不产生副作用多次调用并无大碍。真正需要警惕的是——build里不要放网络请求。如果每次build都触发请求页面跳转动画时你就在狂拉接口既浪费流量又掉帧。正确做法是网络请求放initState或者FutureBuilder的future里build只根据已有状态渲染。7.2 Navigator报“duplicate page route”有时快速双击跳转按钮会报“duplicate page route”错误。原因是同一时刻同一个页面被push了两次。解决方法有三层按钮加防重复点击逻辑比如设置一个冷却时间或根据页面是否已经在栈中决定是否跳转。在跳转前用Navigator.of(context).canPop()做判断或者记录当前路由名、避免相同路由连续入栈。更彻底的方案是统一封装跳转方法在里面加一个“是否已存在同名路由”的判断。实战经验是防重复点击逻辑一定要有而且不能只靠业务侧自觉。因为线上会有极快的手速或者网络卡顿时用户反复点击一旦页面重复入栈用户返回时就会看到“多返回了一层”的怪象。7.3 从Index页面路由到Tab页内部之前讲嵌套Navigator时提过这个问题。最常见的错误表现是从某个Tab的子页面里Navigator.push跳到一个新页面结果发现底部Tab切换后新页面被“卡住”了怎么返回都回不去。排查思路是先搞清楚当前使用的到底哪个Navigator。如果页面是从context里Navigator.of(context)拿到的导航器那它可能属于最近的上层Navigator也可能是全局的。打印一下栈里路由数量就知道了final navigator Navigator.of(context); debugPrint(栈中路由数: ${navigator.widget.pages.length});如果栈中路由数不符合预期就要检查页面是否被嵌套在其他Navigator中。封装统一的跳转工具类并在工具类里明确“由哪个Navigator执行跳转”是避免这类混乱最有效的办法。7.4 返回时拿不到参数Navigator.pop(context, result)弹回后在上一页await处拿到的却是null很多新手当场蒙圈。大概率是下面几个原因之一页面通过系统返回键退出没有执行pop(result)所以返回值为null。pop时被另一个页面“截胡”——比如在PopScope的onPopInvokedWithResult里又手动pop了一次把结果覆盖了。跳转用的不是同一个Navigator你在子Navigator里push却在全局Navigator上等待结果。排查方法在await处打印调试日志看是整条链路没返回还是返回了null。如果是前者重点检查页面退出方式如果是后者重点检查pop调用位置和参数。7.5 命令式路由在异常状态下的白屏开发过程中如果你在未登录状态下通过某种方式pushNamed了一个需要登录态的页面而这个页面在build里直接操作了登录态数据比如读取用户Token就会出现空指针或白屏。尽管业务上已经有了“跳转登录页”的守卫但页面自身的空安全校验绝对不能省。我一般会在每个页面的build开头做一次核心依赖检查缺少数据时渲染一个统一的重试/空态组件。这属于“路由之外的防御”。把“路由能不能进”和“进来后能不能渲染”两件事分开处理代码的稳健性会提升很多。8. 导航与路由真正理解它的角色回到最开始那个话题导航与路由在Flutter里到底是个什么角色它不是“点按钮跳页面”的工具函数也不是“给页面取个名字”的注册表。它是整个App页面组织方式的抽象是你写业务代码之前最先应该想清楚的事情。你的App是简单两三层的页面结构还是多Tab多模块深链接的复杂结构决定了你选Navigator 1.0、go_router还是裸写Navigator 2.0。选型没有绝对的对错只有适配场景与否。我自己带项目的习惯是小项目直接Navigator 1.0清爽不折腾大项目优先go_router路由表统一管理、深浅链接都方便只有遇到非常特殊的定制需求才会深入Navigator 2.0原生的RouterDelegate层写定制逻辑。最后分享一个我在实际开发中觉得特别值得养成的习惯给导航层单独写一个工具类比如AppNavigator所有页面跳转、返回、替换、清理栈的操作都走这个类。这样即使未来把路由方案从Navigator 1.0换成go_router也只需要改这一个文件业务代码几乎不用动。这个习惯帮我度过了好几次“导航方案重构”的艰难时期也让我带的团队成员没有因为导航API变动而大面积返工。导航与路由这门课学完不算本事能在项目里设计出一套适合自己的组织方案才算真正吃透了。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。