Flutter 路由越写越乱?用强类型路由把参数和转场收回来

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, 路由设计, 页面转场