Flutter 路由越写越乱?用强类型路由把参数和转场收回来
导语
Flutter 项目小的时候,路由通常很好写:context.push('/detail?id=1'),页面里再解析参数。项目一大,字符串路由的代价就出来了:参数漏传、类型不一致、路径散落、转场重复、返回逻辑难测。问题不一定马上爆,但每次改页面都心里发虚。
这篇文章介绍一种更工程化的路由组织方式:用 GoRouter 的强类型路由承载路径和参数,用统一的 buildPage 管理转场,让页面跳转从"拼字符串"回到"调用对象"。
背景 / 遇到的问题
商业项目里的路由不只是打开页面。它通常还包括:
- 必填参数和可选参数;
- WebView、详情页、登录页、首页 tab 等不同页面形态;
- iOS 风格转场、无转场、弹窗式转场;
- Android 返回键拦截;
- 深链和外部 URL;
- 登录失效后的统一跳转。
如果所有路由都靠字符串维护,几个问题会反复出现:
- 路径改名后搜索不完整;
- 参数从
int变成String时调用方不报错; - 多个页面复制同一段转场代码;
- 页面返回逻辑散在各处;
- 测试和重构都缺少类型提示。
强类型路由的思路是,把"路径 + 参数 + 页面构建 + 转场"收敛到一个 Route 类里。
核心方案
先看一个简化后的路由定义。
dart
// 示意代码
part 'app_router.g.dart';
@TypedGoRoute<DetailRoute>(path: '/detail/:id')
class DetailRoute extends GoRouteData with _$DetailRoute {
const DetailRoute({
required this.id,
this.source = '',
});
final String id;
final String source;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return CupertinoPage(
child: DetailPage(id: id, source: source),
);
}
}
调用时不再手写路径:
dart
// 示意代码
DetailRoute(id: item.id, source: 'list').push(context);
这带来三个直接收益。
第一,必填参数变成编译期约束。id 如果漏传,调用方直接无法通过分析,而不是等到运行时页面拿到空参数。
第二,参数类型更明确。比如某个页面需要 int pageIndex,强类型路由可以直接声明为 int。调用方传错类型时,IDE 和 analyzer 会先拦下来。
第三,路径集中在路由类上。页面不需要知道自己的 URL 怎么拼,业务代码只表达"我要去哪个页面,并带什么参数"。
转场也可以统一收敛。比如项目里常见的淡入、缩放、弹窗式转场,可以封装成几个构建函数。
dart
// 示意代码
CustomTransitionPage<void> buildScaleFadePage(Widget child) {
return CustomTransitionPage(
transitionDuration: const Duration(milliseconds: 280),
reverseTransitionDuration: const Duration(milliseconds: 200),
transitionsBuilder: (context, animation, secondaryAnimation, child) {
final fade = CurvedAnimation(
parent: animation,
curve: Curves.easeOutCubic,
);
final scale = Tween<double>(begin: 0.92, end: 1).animate(fade);
return FadeTransition(
opacity: fade,
child: ScaleTransition(scale: scale, child: child),
);
},
child: child,
);
}
路由里直接复用:
dart
// 示意代码
@TypedGoRoute<DialogRoute>(path: '/dialog')
class DialogRoute extends GoRouteData with _$DialogRoute {
const DialogRoute();
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return buildScaleFadePage(const DialogLikePage());
}
}
这样做之后,页面转场不再散落在各个调用点。以后要调整动效时,也能按转场类型统一修改。
还有一种容易被忽略的能力:路由类可以承载退出逻辑。比如首页需要处理 Android 返回键,可以在路由数据里实现 onExit,把"当前路径是否首页""是否二次返回"这些规则集中起来。
dart
// 示意代码
class MainRoute extends GoRouteData with _$MainRoute {
const MainRoute({this.tab});
final MainTab? tab;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
return NoTransitionPage(child: MainPage(tab: tab));
}
@override
FutureOr<bool> onExit(BuildContext context, GoRouterState state) async {
if (!isCurrentHome(context, state)) return true;
return await confirmExitByDoubleBack(context);
}
}
业务页面只关心 UI,本该属于导航系统的规则就留在路由层。
关键细节 / 踩坑点
第一,强类型路由依赖生成代码。新增或修改 @TypedGoRoute 后,要更新生成文件。否则调用方可能找不到新的 Route 扩展方法。
第二,不要让路径字符串继续扩散。迁移到强类型路由后,业务代码里应该尽量使用 SomeRoute(...).go(context) 或 push(context),而不是继续 context.push('/somewhere')。否则强类型约束只覆盖了一半。
第三,路由参数要克制。不是所有页面状态都适合放进路由参数。适合放路由的是"页面身份"和"可恢复入口",比如详情 ID、来源、tab;不适合放的是大对象、临时 UI 状态、复杂筛选对象。大对象可以放 Provider、缓存或页面内部状态。
第四,转场函数要按体验归类,而不是每个页面复制一份。常见分类可以是无转场、平台默认、柔和淡入、弹窗缩放、半透明遮罩。每种保留一个清晰实现,页面按需要选择。
第五,深链和 WebView 要特别注意参数编码。强类型不代表可以忽略 URL 编码。凡是可能来自外部的 URL、title、来源字段,都应该清楚区分 path parameter 和 query parameter。
总结
强类型路由的意义不是让路由文件更"高级",而是把跳转参数、页面构建、转场和退出规则集中管理。项目规模越大,它越能减少字符串路由带来的隐性风险,让重构和协作更有底气。
标签
Flutter, GoRouter, TypedGoRoute, 路由设计, 页面转场