flutter 使用go_router 和 page_transition定义页面动画

强强联合!Flutter 中 GoRouter 与 PageTransition 的完美结合指南

在 Flutter 开发中,官方推荐的路由管理库非 go_router 莫属。它不仅支持声明式路由、深层链接(Deep Linking),还能很好地配合 build_runner 实现类型安全的注解路由。然而,go_router 默认的页面切换动画在某些设计需求下显得较为单一。

为了让应用拥有更丰富的转场特效,我们通常会引入 page_transition 库。那么,如何将 GoRouter 与 PageTransition 优雅地结合在一起?又如何做到"一次定义,全局生效"或兼容"注解路由(GoRouter Builder)"呢?

本文将由浅入深,为您提供全套的解决方案,并在文末附带进阶的 mixin 平台自适应扩展。


🛠️ 前期准备

首先,在项目的 pubspec.yaml 中引入所需的依赖:

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  go_router: ^14.0.0      # 请根据项目实际情况选择合适版本
  page_transition: ^2.1.0 # 丰富的页面过渡动画库

基础篇:在单个路由中自定义转场动画

go_router 提供了 pageBuilder 属性,允许我们返回一个 CustomTransitionPage。我们可以直接在 transitionsBuilder 中借用 page_transition 的动画效果。

less 复制代码
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';

final GoRouter _router = GoRouter(
  initialLocation: '/',
  routes: [
    GoRoute(
      path: '/',
      builder: (context, state) => const HomeScreen(),
    ),
    GoRoute(
      path: '/detail',
      pageBuilder: (context, state) {
        return CustomTransitionPage(
          key: state.pageKey, // 🔴 极其重要:必须传入 pageKey 以保持路由状态和避免动画异常
          transitionDuration: const Duration(milliseconds: 400),
          reverseTransitionDuration: const Duration(milliseconds: 400),
          child: const DetailScreen(),
          transitionsBuilder: (context, animation, secondaryAnimation, child) {
            // 使用 PageTransition 提供的仿 iOS 右滑、渐变、缩放等任意效果
            return PageTransition(
              type: PageTransitionType.rightToLeft,
              child: child,
            ).buildTransitions(context, animation, secondaryAnimation, child);
          },
        );
      },
    ),
  ],
);

进阶篇:一次定义,全局所有页面生效

如果每个页面都要写一遍 pageBuilder 显然过于繁琐。以下提供两种全局配置方案,分别对应非注解和注解(GoRouter Builder)的开发模式。

方案 A:通过 ThemeData 全局无感配置(最推荐 ⭐️⭐️⭐️⭐️⭐️)

如果你想省心省力,不修改任何路由层的代码,通过修改 MaterialApp 的全局主题是最优雅的解法。由于 go_router 默认的 builder 会跟随系统主题走,因此我们直接拦截全局的 PageTransitionsTheme 即可。

scala 复制代码
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';

// 1. 自定义一个通用的 PageTransitionsBuilder
class CustomPageTransitionsBuilder extends PageTransitionsBuilder {
  final PageTransitionType transitionType;

  const CustomPageTransitionsBuilder({this.transitionType = PageTransitionType.rightToLeft});

  @override
  Widget buildTransitions<T>(
    PageRoute<T> route,
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
    Widget child,
  ) {
    return PageTransition(
      type: transitionType,
      child: child,
    ).buildTransitions(context, animation, secondaryAnimation, child);
  }
}

// 2. 在 MaterialApp 中配置全局主题
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(
      routerConfig: _router,
      theme: ThemeData(
        pageTransitionsTheme: const PageTransitionsTheme(
          builders: {
            // 为主流平台配置统一的 PageTransition 动画
            TargetPlatform.android: CustomPageTransitionsBuilder(transitionType: PageTransitionType.rightToLeft),
            TargetPlatform.iOS: CustomPageTransitionsBuilder(transitionType: PageTransitionType.rightToLeft),
          },
        ),
      ),
    );
  }
}

// 3. 此时路由定义保持最简,直接使用 builder 就会自动带上动画!
final GoRouter _router = GoRouter(
  routes: [
    GoRoute(path: '/', builder: (context, state) => const HomeScreen()),
    GoRoute(path: '/detail', builder: (context, state) => const DetailScreen()),
  ],
);

方案 B:继承 GoRoute 自定义类(适合非注解模式下的灵活定制)

如果你不想动全局的 ThemeData(以免影响到系统弹窗或其他第三方组件),你可以通过面向对象的方式,封装一个 AnimatedGoRoute

less 复制代码
class AnimatedGoRoute extends GoRoute {
  AnimatedGoRoute({
    required super.path,
    required Widget Function(BuildContext context, GoRouterState state) builder,
    PageTransitionType defaultTransitionType = PageTransitionType.rightToLeft, // 默认动画
    super.routes,
  }) : super(
          pageBuilder: (context, state) => CustomTransitionPage(
            key: state.pageKey,
            child: builder(context, state),
            transitionsBuilder: (context, animation, secondaryAnimation, child) {
              return PageTransition(
                type: defaultTransitionType,
                child: child,
              ).buildTransitions(context, animation, secondaryAnimation, child);
            },
          ),
        );
}

// 使用时:把 GoRoute 替换为 AnimatedGoRoute
final GoRouter _router = GoRouter(
  routes: [
    GoRoute(path: '/', builder: (context, state) => const HomeScreen()), // 首页不需要动画
    AnimatedGoRoute(path: '/detail', builder: (context, state) => const DetailScreen()), // 默认动画
    AnimatedGoRoute(
      path: '/profile', 
      defaultTransitionType: PageTransitionType.bottomToTop, // 覆盖并自定义特定动画
      builder: (context, state) => const ProfileScreen()
    ),
  ],
);

终极篇:在注解路由 @TypedGoRoute 中结合使用

很多大型项目为了保证路由安全性,会使用 go_router_builder 生成路由代码。针对注解路由,我们可以通过重写 GoRouteDatabuildPage 方法来实现动画注入。这里有两种设计风格:

方式一:基于"基类继承"的动画注入

我们可以封装一个带动画的路由基类 AnimatedRouteData

scala 复制代码
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:page_transition/page_transition.dart';

abstract class AnimatedRouteData extends GoRouteData {
  const AnimatedRouteData();

  Widget buildChild(BuildContext context, GoRouterState state);

  PageTransitionType get transitionType => PageTransitionType.rightToLeft;

  @override
  Page<void> buildPage(BuildContext context, GoRouterState state) {
    return CustomTransitionPage<void>(
      key: state.pageKey,
      child: buildChild(context, state),
      transitionsBuilder: (context, animation, secondaryAnimation, child) {
        return PageTransition(
          type: transitionType,
          child: child,
        ).buildTransitions(context, animation, secondaryAnimation, child);
      },
    );
  }
}

在使用时,让生成的路由类继承自它,并实现 buildChild 即可:

scala 复制代码
@TypedGoRoute<DetailRoute>(path: '/detail')
class DetailRoute extends AnimatedRouteData {
  @override
  Widget buildChild(BuildContext context, GoRouterState state) => const DetailScreen();
}

🔥 硬核拓展:基于 Mixin 的平台自适应过渡动画(更推荐 ⭐️⭐️⭐️⭐️)

由于 Dart 语言只支持单继承,如果你的路由类已经有了基类(或者为了保持结构干净),使用 mixin(混入) 的方式注入动画比继承更具有组合灵活性。

同时,我们可以在这里直接实现原生平台的自适应动画:iOS 上展示苹果官方的右侧侧滑切页,Android 上展示底部滑入加渐显效果。

1. 定义自适应 Mixin 和构建函数

Dart

less 复制代码
import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

/// 混入此 mixin 的路由自动获得平台自适应过渡动画,无需手动覆写 [buildPage]。
mixin PlatformPageRoute on GoRouteData {
  @override
  Page<void> buildPage(BuildContext context, GoRouterState state) =>
      platformPage(
        key: state.pageKey,
        child: build(context, state),
      );
}

CustomTransitionPage<T> platformPage<T>({
  required Widget child,
  LocalKey? key,
  String? name,
}) {
  final isIOS = Platform.isIOS;

  return CustomTransitionPage<T>(
    key: key,
    name: name,
    child: child,
    transitionsBuilder: (context, animation, secondaryAnimation, child) {
      if (isIOS) {
        // iOS 风格:从右侧侧滑切页 (使用原生 CupertinoPageTransition)
        return CupertinoPageTransition(
          primaryRouteAnimation: animation,
          secondaryRouteAnimation: secondaryAnimation,
          linearTransition: false,
          child: child,
        );
      }

      // Android 风格:从底部滑入 + 渐显
      return SlideTransition(
        position: Tween<Offset>(
          begin: const Offset(0, 0.1),
          end: Offset.zero,
        ).animate(CurvedAnimation(
          parent: animation,
          curve: Curves.easeOut,
        )),
        child: FadeTransition(
          opacity: animation,
          child: child,
        ),
      );
    },
  );
}
2. 在注解路由中优雅使用 Mixin

使用该方法时,你的代码极其干净,甚至不需要改变原生的 build 命名方法 ,只需使用 with PlatformPageRoute 一键启用动画增强:

scala 复制代码
// 使用 with 混入动画,代码极其优雅、无破坏性
@TypedGoRoute<SettingsRoute>(path: '/settings')
class SettingsRoute extends GoRouteData with $SettingsRoute, PlatformPageRoute {
  
  @override
  Widget build(BuildContext context, GoRouterState state) => const SettingsScreen();
}

💡 核心避坑指南

  1. key: state.pageKey 是重中之重 :在使用 CustomTransitionPage 时,请务必显式传入 state.pageKey。如果不传,当路由参数(Path Parameters)或查询参数(QueryParams)发生变化时,Flutter 会由于无法识别页面身份,错误地卸载并重新创建页面,引发转场动画闪烁或页面状态丢失。
  2. 动画时间的统筹 :在全局设置时,建议通过 transitionDurationreverseTransitionDuration 严格控制进场与出场时间(通常推荐在 300ms ~ 400ms 之间),可以极大地提升多设备运行下的流畅感。

📝 总结

  • 彻底省心、无侵入 :选择 ThemeData 全局配置
  • 非注解路由、个别定制 :选择 封装 AnimatedGoRoute 继承类
  • 注解路由、喜欢干净组合 :选择 PlatformPageRoute 的 Mixin 混入方案

希望这篇文章能帮助你在 Flutter 路由管理与视觉动画之间找到完美的平衡!如果有任何疑问,欢迎在评论区交流讨论。

相关推荐
多加点辣也没关系1 小时前
JavaScript|第6章:流程控制语句
开发语言·前端·javascript
csj501 小时前
前端基础之《React(11)—state声明式》
前端·react.js
程序员晓琪1 小时前
深入解析:从 Webpack 到 Vite,多入口多端项目的工程化迁移实战
前端·webpack·vite
涛涛ing1 小时前
Vue 3.6 还没正式发布,但前端的方向已经被它定下来了
前端
阿新聊ai1 小时前
Claude Code 为什么总不懂你的项目?缺一张「项目地图」
前端·后端·面试
暗冰ཏོ1 小时前
Spring Boot + Vue3 订单支付模块技术实现:支付单、回调验签、幂等处理与余额扣减
前端·spring boot·后端·vue·微信支付·支付宝支付
Csvn1 小时前
AbortController 不止能取消 Fetch!3 个你不知道的隐藏用法
前端·javascript
爷_2 小时前
AI Coding Agent 沙箱的设计原理解析
前端·人工智能·后端
爱勇宝2 小时前
第 3 章:别把团队设计成一场内卷竞赛
前端·后端·程序员