1. 路由方案选型的现实取舍Flutter auto_route 这套路由管理方案我是在第三个中大型项目上才真正用顺手的。前两个项目一个用原生 Navigator 1.0 硬怼一个用另一套声明式路由库过渡踩完坑之后才明白路由这件事本质上不是“页面跳转怎么写”的问题而是“页面之间的契约怎么保证”的问题。auto_route 的核心价值就在这一点上——它用代码生成把路由表、路径参数、查询参数、返回值类型全部编译成 Dart 类型让“跳错页面”“传错参数”“拿错返回值”这类问题在编译期就报出来而不是等测试同学点到一个冷门入口才崩。这篇内容适合三类人正在用原生 Navigator 但页面栈已经开始失控的用着别的路由库但被字符串路径折磨过的以及刚接触声明式路由、想知道代码生成到底生成了什么的新手。下面我把选型逻辑、生成机制、落地骨架、类型安全细节、嵌套路由和排查经验全部摊开讲。1.1 Navigator 1.0 的命令式路由此刻还行不行原生 Navigator 的命令式写法在小项目里确实够用Navigator.push(context, MaterialPageRoute(builder: (_) DetailPage(id: id)))一行就能跳。问题出在规模上当页面数量过了三十个、带参数跳转的地方超过五十处之后你会发现代码库里散落着一堆MaterialPageRoute改一个页面的构造参数编译器不会告诉你还有哪十几个地方在传旧参数——它只会在运行时抛一个参数不匹配的错误或者更糟参数默默传成了null。这就是命令式路由最要命的地方路由跳转的调用点和页面的构造函数之间没有静态约束。另一个隐痛是页面栈的可见性。Navigator 1.0 里“当前栈里有什么”是一个运行时概念你只能靠Navigator.of(context)去操作想在某个时机判断“用户是不是从深链接进来的”或者“我需不需要在跳转前先弹登录”只能自己维护标志位。声明式路由把这层抽象成了“路由表 导航器状态”路由表是编译期常量状态是可观测对象两者一分开很多事情就好办了。1.2 auto_route 在类型安全上补了哪几刀auto_route 干的事情可以拆成三层来看。第一层是路由表声明化所有页面在一个AutoRouterConfig类里集中登记哪个是初始路由、哪个带路径参数、哪个嵌在 Shell 里一眼能看完不用满项目搜push。第二层是页面身份类型化每个页面通过RoutePage()注解由生成器产出一个对应的XxxRoute类它是PageRouteInfo的子类push的时候传的是这个类的实例而不是一个字符串或者一个匿名 Builder。第三层是参数静态校验页面构造函数的参数会被生成器映射成路由类的构造参数传错类型、漏传必填参数写代码的时候 IDE 就标红了。这三层叠起来效果是“路由调用点变成了强类型函数调用”。我印象最深的一次重构把一个详情页的String id改成DetailArgs args一个包含 id、来源、是否预览的对象改完之后flutter analyze直接列出了十七个需要修改的调用点一个不漏。如果还是命令式写法这十七个点我得靠全局搜索DetailPage(去找而且搜索会漏掉那些被包了一层的封装函数。1.3 引入它的代价构建时间、心智与版本绑定天下没有白拿的类型安全。auto_route 的代价主要有三块我在选型时都会提前跟团队说清楚。第一是构建链路变长。它依赖build_runner生成代码意味着你多了一个dart run build_runner build的步骤改完页面参数如果忘了重新生成IDE 会报一堆“找不到 XxxRoute”的红。团队里统一用 watch 模式dart run build_runner watch -d能缓解大半但 CI 上必须显式跑一次生成再分析否则流水线直接挂。第二是心智负担前移。新同学第一次看到一个页面文件里只有RoutePage()注解、真正的路由类在.page.gr.dart里会有点懵。我的做法是在项目 README 里画一张极简的“三文件关系图”页面文件 → 注解 → 生成文件 → 路由表引用五分钟能讲明白。第三是版本绑定。auto_route主版本升级时auto_route_generator必须跟着升两者版本号不匹配时生成器会直接报错退出。而且 7.x 之后统一成RoutePage()一种注解之前那些MaterialRoutePage、CupertinoRoutePage的写法全部废弃了老项目迁移要改注解。我一般的策略是pubspec.yaml里两个包用相同的插入符号版本约束锁死在大版本内升级单独开分支验证不跟业务改动混在一起。维度原生 Navigator集中式声明路由auto_route路由表可见性散落各处集中集中且强类型参数校验时机运行时部分编译期编译期嵌套/底部导航支持手写中等内置 Shell额外构建步骤无无需要代码生成深链接映射手写一般内置 builder2. 代码生成的底层机制拆解很多人用 auto_route 是照着文档抄配置抄完能跑就不管了。但只要出一次生成失败不知道内部怎么运转就会卡很久。这一章我把它拆开讲透后面排查问题会轻松很多。2.1 注解、Builder 与生成产物三者怎么咬合整条链路是这样的你在页面类上写RoutePage()生成器扫描到这个注解为这个页面生成一个XxxRoute类放在同目录的xxx.page.gr.dart文件里你在路由配置类上写AutoRouterConfig()生成器扫描到它为它生成一个抽象基类$AppRouter旧版本是_$AppRouter放在app_router.gr.dart里这个基类里包含所有页面的 getter 和路由表组装逻辑你的AppRouter类extends $AppRouter并重写routesgetter把XxxRoute.page这种静态引用塞进去。关键的咬合点在XxxRoute.page。生成器为每个页面生成的不只是一个路由类还有一个PageInfo常量它把“路由类的构造参数”和“页面的 Widget 构造函数”绑定在一起。路由表里写AutoRoute(page: ProfileRoute.page, path: /profile/:userId)运行时框架就知道当导航到ProfileRoute(userId: x)时应该构造ProfilePage(userId: x)。这层映射是生成期固定的所以只要路由类和页面构造函数的参数对得上运行时就不会出现“参数没传进去”的情况。理解这一点之后你就会明白为什么改页面构造函数之后必须重新生成生成文件里的映射是快照页面改了但快照没更新两边就对不上了。这时候 IDE 报的错往往指向生成文件内部看起来莫名其妙实际上是快照过期跑一次build_runner build --delete-conflicting-outputs就好了。2.2 生成的 .gr.dart 文件里究竟写了什么我建议每个用 auto_route 的人都花十分钟打开生成文件看一眼不用全看懂看个大概结构就行。以页面层为例profile_page.page.gr.dart里大致是这么个东西// GENERATED CODE - DO NOT MODIFY BY HAND part of profile_page.dart; class ProfileRoute extends PageRouteInfoProfileRouteArgs { ProfileRoute({ required String userId, String? tab, ListPageRouteInfo? children, }) : super( ProfileRoute.name, args: ProfileRouteArgs(userId: userId, tab: tab), initialChildren: children, ); static const String name ProfileRoute; static const PageInfoProfileRouteArgs page PageInfoProfileRouteArgs(name); } class ProfileRouteArgs { const ProfileRouteArgs({required this.userId, this.tab}); final String userId; final String? tab; }看到ProfileRouteArgs这个类就抓到重点了所有参数被聚合进一个 args 对象路由类只是它的包装。这个设计的好处是序列化、深链接解析、状态恢复都能统一走 args。坏处是你不能直接在路由类上放业务逻辑它就是纯粹的数据载体。Router 层的生成文件app_router.gr.dart里则是$AppRouter抽象类包含每个页面的AutoRoute辅助构造、ListAutoRoute get routes的抽象声明以及一些内部的路由查找方法。如果你在路由表里写了AutoRoute(page: ProfileRoute.page)生成器就在基类里预留好对ProfileRoute的引用所以路由表文件必须import到生成文件——这就是为什么 router 文件顶部要有part app_router.gr.dart;。2.3 命名规则与 replaceInRouteName 的坑AutoRouterConfig()有个参数叫replaceInRouteName默认值是Page,Route意思是把页面类名里的Page后缀替换成Route后缀来生成路由类名。ProfilePage生成ProfileRouteProfileRouteArgs也跟着变。听起来很顺但坑集中在两处。坑一后缀不匹配导致类名重复。如果你有个页面叫SettingPage还有个页面叫SettingRoutePage按默认规则前者生成SettingRoute后者生成SettingRouteRoute看起来没冲突。但如果你把规则改成View,Route那SettingView和SettingViewPage就可能撞名。我一般不改默认规则团队统一约定页面类名必须以Page结尾简单粗暴但不会出错。坑二手滑把页面类名改了但忘了重新生成。这时候旧的路由类还在生成文件里新的又没出来路由表里引用的旧类还“存在”编译能过但跳转时你会发现页面根本没渲染或者渲染的是旧逻辑。表现非常迷惑。我的应对办法是改页面类名属于重构操作改完立刻跑一次干净的生成带--delete-conflicting-outputs然后跑一遍flutter analyze和冒烟测试绝不带着旧产物继续写业务。还有一个容易被忽略的点RoutePage()生成的文件和页面文件之间的关系不同 7.x 小版本行为略有差异有的版本生成累加文件、有的版本用独立文件加part。你在第一次接入时只要盯住生成器输出的提示报错说缺part就补part报错说重复定义就清一次产物别死记某个版本的写法。3. 从零搭一套能跑的路由骨架原理讲完接下来是能直接抄作业的部分。我按真实项目里的落地顺序走一遍从依赖到目录再到参数传递每一步都说明为什么这么选。3.1 依赖与目录约定依赖只有四个运行时包auto_route开发期的auto_route_generator、build_runner以及可选的build_runner配套日志工具。版本上我建议两个 auto_route 包用同一个大版本避免生成器与运行时 API 对不上。dependencies: flutter: sdk: flutter auto_route: ^7.8.4 dev_dependencies: build_runner: ^2.4.6 auto_route_generator: ^7.8.3目录上我习惯这么切lib/ app/ router/ app_router.dart // 路由表 AutoRouterConfig app_router.gr.dart // 生成不手改 guards/ auth_guard.dart app.dart // MaterialApp.router 装配 features/ profile/ profile_page.dart // RoutePage 页面 profile_page.page.gr.dart home/ home_page.dart home_page.page.gr.dart shared/ widgets/这样切的好处是路由配置和业务模块物理隔离app/router只认路由类和守卫不认具体 UIfeatures下的页面自己带自己的生成文件增删模块时不会牵一发动全身。我还习惯在.gitignore里不忽略生成文件——虽然理论上能重新生成但新同学拉代码直接能跑、CI 少一步生成失败的排查成本这点体积换得值。3.2 根路由、ShellRoute 与初始路由路由表文件大概长这样import package:auto_route/auto_route.dart; import app_router.gr.dart; import guards/auth_guard.dart; AutoRouterConfig() class AppRouter extends $AppRouter { override ListAutoRoute get routes [ AutoRoute( page: MainShellRoute.page, path: /, initial: true, children: [ AutoRoute(page: HomeRoute.page, path: , initial: true), AutoRoute(page: SearchRoute.page, path: search), AutoRoute(page: ProfileRoute.page, path: profile/:userId), ], ), AutoRoute(page: LoginRoute.page, path: /login), AutoRoute( page: DetailRoute.page, path: /detail/:id, guards: [AuthGuard()], ), ]; }几个关键点值得说明。子路由的路径不要带前导斜杠写search而不是/search因为父路由已经匹配了/子路由是相对拼接的写斜杠会变成绝对路径导致嵌套关系断掉。Shell 的initial: true和内部子路由的initial: true要配对前者告诉框架“这是启动时的外壳”后者告诉框架“这是外壳里默认展示的一页”只写一个的话冷启动可能白屏。带参数的路径段用冒号前缀/profile/:userId这个名字会被生成器拿去和页面构造函数里的同名字段自动匹配。装配层在app.dartfinal _appRouter AppRouter(); class App extends StatelessWidget { const App({super.key}); override Widget build(BuildContext context) { return MaterialApp.router( routerConfig: _appRouter.config(), builder: (context, child) child ?? const SizedBox.shrink(), ); } }_appRouter我放在类外做单例而不是放在build里 new原因是build每次重建都会造一个新路由实例页面栈状态就丢了。这一点我在早期项目上踩过表现为“每次父级 setState 页面就回退到首页”查了半天才反应过来。3.3 参数三种传递方式的取舍auto_route 里参数有三条路路径参数、查询参数、extra。它们的区别不是技术细节而是语义和责任的区别。路径参数PathParam是页面身份的一部分参与到 URL 里比如/detail/42。它必须能从字符串反序列化默认支持String、int、bool等必填且决定了页面“是不是同一个页面”。用途是那种“没有这个 id 这个页面就没有意义”的场景。查询参数QueryParam是可选的修饰信息比如/search?qfluttersortnew。它可以有默认值不参与页面身份判定。RoutePage() class SearchPage extends StatelessWidget { const SearchPage({QueryParam(q) this.keyword, QueryParam(sort) this.sortBy new, super.key}); final String? keyword; final String sortBy; }extra是纯内存传递不进 URL适合传对象、回调、不可序列化的东西比如从列表页进详情页时把整条数据对象带过去避免详情页再查一次。context.router.push(DetailRoute(id: 42, data: item));它的代价是页面刷新、深链接直达、状态恢复时extra一定是null所以任何依赖extra的页面都必须处理 null 分支。我的经验规则是能被 URL 完整描述的状态就走路径参数和查询参数只有纯体验优化避免重复请求才用extra并且页面里必须有“extra 为空时自己去拉数据”的兜底逻辑。这条规则在小程序、Web、桌面端同时存在的项目里尤其重要因为 Web 端用户按 F5 刷新是常态。4. 类型安全落地把崩溃挡在编译期前面的是骨架这一章是让骨架真正稳定的肌肉。类型安全不是加个注解就自动获得的得在写法上主动配合。4.1 PageRouteInfo 泛型与强类型参数对象PageRouteInfo是带泛型的PageRouteInfoTArgs而每个具体的路由类都是PageRouteInfoXxxRouteArgs。这意味着push的时候框架知道这个页面接受什么参数对象。这个能力最实用的地方是批量传递当某个页面的参数超过四个之后把它们收进一个freezed或普通不可变类里路由类自动变成PageRouteInfoXxxArgs调用方传一个对象就行将来参数再加两个调用点不用大改。class FilterArgs { const FilterArgs({this.keyword , this.priceMin, this.priceMax, this.tags const []}); final String keyword; final int? priceMin; final int? priceMax; final ListString tags; }要注意的是参数对象本身不会被生成器插手它只负责给 args 类当字段。所以如果参数对象里有可变集合、可变类路由类的“值语义”就被破坏了做深链接序列化时也可能出问题。我一般要求参数对象的所有字段都是final集合字段用const默认值或者List.unmodifiable包一层。另一个使用习惯路由类不在业务代码里到处 import 生成文件。生成文件是通过路由表文件的part暴露的业务页面 import 路由表文件即可这样模块边界更清晰也避免了循环依赖。4.2 返回值与 pop 的正确姿势页面返回结果是路由里最容易被写烂的部分。命令式写法里是Navigator.pushT返回FutureT?声明式里同样是这个模型但参数化了之后更安全final result await context.router.pushEditResult(EditRoute(item: item)); if (result null) return; // 用户直接返回了 applyResult(result);页面内的返回context.router.pop(EditResult(saved: true, id: item.id));这里有个关键约束push 时指定的泛型类型必须和 pop 时传的对象类型一致否则result永远是 null。我见过有人 push 时写Stringpop 时传了个Map然后抱怨“返回值拿不到”。编译器管不到这条链路因为它跨了页面边界只能靠约定和代码评审守住。我的做法是在路由类旁边加一行注释写明返回类型或者更彻底一点把返回值类型也做成一个命名类让类型名本身说明意图。还有一个容易踩的细节await push之后如果做异步操作一定要先判断context.mounted。用户可能在你 await 期间已经退出了这个页面直接操作 context 会抛异常。这个是 Flutter 的通用常识但在路由场景里尤其高频因为“等用户编辑完再刷新列表”这个模式太常见了。4.3 守卫、重定向和深链接的类型化写法守卫Guard是 auto_route 比多数路由库强的地方它把“能不能进这个页面”从业务代码里抽出来做成了可复用的类。class AuthGuard extends AutoRouteGuard { override void onNavigation(NavigationResolver resolver, StackRouter router) { final loggedIn AuthService.instance.isLoggedIn; if (loggedIn) { resolver.next(true); return; } router.push( LoginRoute( onLoginResult: (ok) { if (ok) { resolver.next(true); } else { resolver.next(false); } }, ), ); } }resolver.next(true)放行resolver.next(false)拦截并回退。这里最容易写错的是忘记处理异步如果你在守卫里await了网络请求然后在 await 之后调用resolver.next必须确保这个 resolver 没有被重复调用也不要漏调。框架里对 resolver 的调用是“一次性”的调两次会出问题。我的写法是先把结果算出来再调不在 await 的间隙里调。重定向Redirect用于“这个页面不该出现在这里”比如登录后不该还能进登录页class AuthRedirect extends AutoRedirectGuard { override void onNavigation(NavigationResolver resolver, StackRouter router) { if (AuthService.instance.isLoggedIn) { router.replaceAll([const HomeRoute()]); } } }深链接的映射靠deepLinkBuilderoverride DeepLink? deepLinkBuilder(Uri uri) { if (uri.pathSegments.isEmpty) return null; switch (uri.pathSegments.first) { case detail: return DeepLink([DetailRoute(id: uri.pathSegments[1])]); case profile: return DeepLink([MainShellRoute(children: [ProfileRoute(userId: uri.pathSegments[1])])]); default: return null; } }这段代码的坑在于深链接进入时的页面栈必须完整。如果你直接DeepLink([DetailRoute(...)])用户按返回键会直接退出应用因为他底下什么都没有。正确做法是把该有的父级页面一起放进列表里让返回行为符合直觉。这个细节在 Web 端和外链唤起场景里尤其明显我第一次上线就因为这个被反馈“按返回直接退出了”。5. 嵌套路由与底部导航的实战组合底部导航加页面栈是绝大多数 App 的标配也是路由方案的压力测试点。auto_route 提供了两套打法用错了会很别扭。5.1 ShellRoute 与 AutoTabsScaffold 的边界AutoRoute里带children的那个父页面如果自己手写Scaffold加bottomNavigationBar那叫 Shell 模式切换 tab 靠context.router操作。AutoTabsScaffold是封装好的快捷方式它内部帮你处理了TabsRouter的创建和索引管理。我的判断标准很简单底部导航的样式能不能用标准BottomNavigationBar或NavigationBar表达。能就用AutoTabsScaffold省一堆样板代码不能比如中间凸起一个圆形按钮、或者带自定义动画指示器就退回 Shell 模式自己搭。AutoTabsScaffold( routes: const [HomeRoute(), SearchRoute(), ProfileRoute(userId: me)], transitionBuilder: (context, child, animation) FadeTransition(opacity: animation, child: child), bottomNavigationBuilder: (_, tabsRouter) { return BottomNavigationBar( currentIndex: tabsRouter.activeIndex, onTap: tabsRouter.setActiveIndex, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home_outlined), label: 首页), BottomNavigationBarItem(icon: Icon(Icons.search), label: 搜索), BottomNavigationBarItem(icon: Icon(Icons.person_outline), label: 我的), ], ); }, )注意routes里的ProfileRoute(userId: me)——tab 页面同样需要参数如果某个参数在 tab 初始化时拿不到就得给个占位值或者用查询参数延迟解析。我见过一个项目在 tab 路由里放了必填的userId结果用户未登录时直接崩因为在构建routes列表的那一刻就得把参数凑齐。这类问题的解法是tab 级页面的必需参数尽量少需要用户信息就在页面内部读状态而不是从路由参数传。5.2 状态保持、索引同步与返回键处理三个高频问题我按发生频率排。状态保持。默认情况下切走再切回来tab 页面会重建列表滚动位置全丢。解决办法有两个层次轻量的用PageStorageKey包住可滚动组件彻底的在AutoRoute上加maintainState: true让页面实例常驻。AutoRoute(page: HomeRoute.page, path: , initial: true, maintainState: true),maintainState的代价是内存页面里如果挂了视频、地图、大列表全都常驻内存低端机上很容易 OOM。我的策略是列表型 tab 开maintainState媒体型 tab 不开改用状态恢复或重新加载并在设计上接受“切回来要重新拉一次”的体验损失。索引同步。外部代码想在某个时机切到指定 tab用context.router.innerRouterOfTabsRouter(MainShellRoute.name)?.setActiveIndex(2)。这个写法里MainShellRoute.name是路由名写错了会返回 null 而静默失败所以我会在调试期包一层断言。另一种更稳的做法是给 Shell 加个override的TabsRouter引用通过全局的 router 实例去拿但那样耦合更高我不推荐。返回键处理。Android 上用户按返回键时期望行为是“先回退到第一个 tab再退出”。AutoTabsScaffold本身不处理这个需要包一层PopScopePopScope( canPop: false, onPopInvoked: (didPop) { if (didPop) return; if (tabsRouter.activeIndex ! 0) { tabsRouter.setActiveIndex(0); } else { SystemNavigator.pop(); } }, child: ..., )这段代码看着简单但canPop: false会让子页面的pop也受影响记得在真正需要弹出的地方判断当前路由层级。我在一个项目上就因为这个导致二级页面的返回按钮全部失效查了半天才发现是 Shell 层的PopScope拦截了。6. 常见问题与排查实录这一章是我这两年里攒下来的排查笔记按阶段分类遇到问题时按表找比从头想快得多。6.1 生成阶段的报错与修复生成阶段的错误信息往往不够直白我把最常见几类整理如下。报错关键词真实原因处理方式Could not find the generated file页面加了RoutePage()但没跑生成跑dart run build_runner build -dConflicting outputs旧产物没清与新产物同名加--delete-conflicting-outputs参数The name XxxRoute isnt a type路由表没part生成文件或生成的类名与预期不符检查part指令和replaceInRouteNameMissing concrete implementation改了路由表但生成文件还是旧的重新生成后再分析Duplicate class两个页面映射到了同一个路由类名重命名页面类保证前缀唯一我踩过最坑的一次是“生成成功但运行时报找不到页面”。排查下来是app_router.gr.dart里还残留着上一个版本的XxxRoute引用因为那次我是用 IDE 的重构改的类名旧生成文件没被清理新老两份同时在路由表指向了旧的。从此我形成了固定动作改页面类名之后先删掉所有.gr.dart再全量生成一次。多花二十秒省两小时。另一个建议是把生成命令写进Makefile或者脚本里团队统一用同一条命令避免“你那边能跑我这边不行”的扯皮。命令里带上-d和固定的--build-filter如果项目大可以只生成路由相关文件构建时间能砍掉一大半。6.2 运行期异常速查表运行期的坑更隐蔽因为不报错、只是行为不对。现象大概率原因验证方法冷启动白屏初始路由只标了外层没标内层检查 Shell 和子路由的initial跳转后页面没渲染路由类与页面参数不匹配构造失败被吞打开 debug 日志看构造异常返回值永远是 nullpush/pop 的泛型不一致统一返回值类型为命名类切换 tab 后返回键直接退出Shell 的PopScope没配见上一章的写法深链接进入后返回直接退出DeepLink 里缺父级页面补齐页面栈热重载后路由表不更新生成文件变更没被热重载捕获完全重启应用关于“热重载不生效”这条我要多说两句。改路由表增删页面、改路径、改守卫属于结构性变更热重载很多时候不会重建AppRouter实例表现是“代码改了但跳转还是老样子”。我的习惯是只要动的是路由表就按热重启处理不要浪费时间怀疑人生。6.3 测试与长期维护的经验路由这层如果完全不做测试出问题基本靠用户反馈。我一般做三件事成本不高但收益明显。第一件是路由表的冒烟测试写一个测试把AppRouter().routes遍历一遍断言每个AutoRoute的path唯一、每个 Shell 子路由的path不以斜杠开头、带参数的路径里参数名和页面构造函数字段能对上。这类测试写一次跑几十毫秒能挡住大部分低级错误。第二件是守卫的单测AutoRouteGuard是纯逻辑可以注入假的登录状态断言resolver.next收到的是 true 还是 false。用 mock 把 resolver 包一层记录调用参数即可不需要真起一个 App。第三件是深链接的集成测试给一组典型 URL断言deepLinkBuilder返回的DeepLink里页面栈的层级和顺序符合预期。深链接是最容易在迭代中被改坏又最不容易被发现的功能因为开发期几乎没人手动触发它。长期维护上还有两个小习惯我坚持到现在。一是路由表加注释每个AutoRoute上方一行注释写明“这个页面从哪进、返回什么”半年后回来看能省很多时间。二是生成文件纳入版本管理但加一条 CI 检查CI 里重新生成一次如果git diff有变化就说明有人改了页面但忘了提交生成文件直接失败。这个检查帮我们抓到过好几次“本地能跑、别人拉下来编译失败”的情况。我个人在实际操作中的体会是auto_route 这类“代码生成 强类型”的方案收益曲线不是线性的项目小的时候它只是多了一步生成感觉不到好处页面过了二十个、参与的人超过三个之后它带来的编译期保护会突然变得极其值钱。真正决定它好不好用的不是库本身而是团队有没有把“改页面必须重新生成”“路由表集中管理”“返回值统一类型”这几条约定执行下去。约定执行到位路由层几乎是项目里最省心的部分执行不到位那它就会变成另一种形式的字符串魔法只不过魔法藏在了.gr.dart文件里。
阅读完成 · 觉得有帮助?