强强联合!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 生成路由代码。针对注解路由,我们可以通过重写 GoRouteData 的 buildPage 方法来实现动画注入。这里有两种设计风格:
方式一:基于"基类继承"的动画注入
我们可以封装一个带动画的路由基类 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();
}
💡 核心避坑指南
key: state.pageKey是重中之重 :在使用CustomTransitionPage时,请务必显式传入state.pageKey。如果不传,当路由参数(Path Parameters)或查询参数(QueryParams)发生变化时,Flutter 会由于无法识别页面身份,错误地卸载并重新创建页面,引发转场动画闪烁或页面状态丢失。- 动画时间的统筹 :在全局设置时,建议通过
transitionDuration和reverseTransitionDuration严格控制进场与出场时间(通常推荐在300ms ~ 400ms之间),可以极大地提升多设备运行下的流畅感。
📝 总结
- 彻底省心、无侵入 :选择 ThemeData 全局配置。
- 非注解路由、个别定制 :选择 封装
AnimatedGoRoute继承类。 - 注解路由、喜欢干净组合 :选择
PlatformPageRoute的 Mixin 混入方案。
希望这篇文章能帮助你在 Flutter 路由管理与视觉动画之间找到完美的平衡!如果有任何疑问,欢迎在评论区交流讨论。