「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
本文翻译自 typed-routing
水平太烂~ 翻到怀疑人生~ 不吝赐教~
[译]Flutter Favorite之路由包go_router - 基础篇 - 掘金 (juejin.cn)
类型化路由方案
下面是类型化路由系统的一个方案。其中一些是在 PR 中实现的,但是还没有完成,现在也没有作为 go_router 包的一部分被支持。写下这些是为了做自己喜欢做的事的一种方式: 聚焦于要写的客户端代码,而不是只聚焦于实现。
目标
go_router 中的路由根本上依赖URI信息中基于字符串的位置来匹配一个或多个页面构建器,每一个需要0个或多个参数作为路径和位置部分的参数传递。go_router 做了一个很好的工作,通过 GoRouterState 的 params 和 queryParams 属性使路径和查询参数可用,但是通常页面构建器必须首先解析参数为非字符串的类型,例如:
GoRoute(
path: ':authorId',
builder: (context, state) {
// require the authorId to be present and be an integer
final authorId = int.parse(state.params['authorId']!);
return AuthorDetailsScreen(authorId: authorId);
},
),
该示例中,authorId 参数 a) 必须的 b) 必须是 int 。
尽管这样,这些需求直到在运行时都不会被检查,代码写起来很简单,但不是类型安全的。例如:
void _tap() => context.go('/author/a42'); // error: `a42` is not an `int`
因为 Dart 是一种静态类型语言,我们更喜欢在编译时捕获错误而不是等到运行时。类型化路由方案是为了提供一个方式,它可以定义必需的变量和可选的变量用于指定的路由消费,然后使用代码生成器做一些苦差事,如写一堆需要我们自己实现的 go 、 push 和 location 的样板代码。
今天在 [我们的试验性实现]中用于实现该工作的代码生成器是 build_runner 包,但是计划是当它们可用时移动到 Dart macros 。
定义一个路由
定义每个路由作为一个类继承 GoRouteData 然后覆写 build 方法。
class PersonRoute extends GoRouteData {
PersonRoute({required this.fid, required this.pid});
final String fid;
final String pid;
@override
Widget build(BuildContext context) => PersonScreen(fid: fid, pid: pid);
}
所需的参数从路由树中定义的路由的 path 中拉取。
路由树
路由树在每个顶级路由定义为一个属性。
@TypedGoRoute<HomeRoute>(
path: '/',
routes: [
TypedGoRoute<FamilyRoute>(
path: 'family/:fid',
routes: [
TypedGoRoute<PersonRoute>(
path: 'person/:pid',
),
],
),
],
)
class HomeRoute extends GoRouteData {...}
class FamilyRoute extends GoRouteData {...}
class PersonRoute extends GoRouteData {...}
@TypedGoRoute<LoginRoute>(path: '/login')
class LoginRoute extends GoRouteData {...}
GoRouter 初始化
代码生成器聚集所有的顶级路由到 $appRoutes 的单个列表中,用于初始化 GoRouter 实例。
final _router = GoRouter(routes: $appRoutes);
Error 构建器
也可以使用类型化路由来提供一个 Error 构建器:
class ErrorRoute extends GoRouteData {
ErrorRoute({required this.error});
final Exception error;
@override
Widget build(BuildContext context) => ErrorScreen(error: error);
}
有了这个,也以如下提供 errorBuilder 参数:
final _router = GoRouter(
routes: $appRoutes,
errorBuilder: (c, s) => ErrorRoute(s.error!).build(c),
);
导航
导航使用代码生成器提供的 go 或者 push 方法:
void _tap() => PersonRoute(fid: 'f2', pid: 'p1').go(context);
如果弄错了,编译器就会抱怨:
// error: missing required parameter 'fid'
// 错误: 缺少必需参数 'fid'
void _tap() => PersonRoute(pid: 'p1').go(context);
这个,当然,是类型化路由的全部;编译器在出错时会让我们知晓。
查询参数
可选的参数指明查询参数:
class LoginRoute extends GoRouteData {
LoginRoute({this.from});
final String? from;
@override
Widget build(BuildContext context) => LoginScreen(from: from);
}
附加参数
路由可以消费一个附加参数,通过类型化构造器的特殊名称 $extra 的参数:
class PersonRouteWithExtra extends GoRouteData {
PersonRouteWithExtra(this.$extra);
final Person $extra;
@override
Widget build(BuildContext context) => PersonScreen(person: $extra);
}
传递附加参数作为类型化对象:
void _tap() => PersonRouteWithExtra(Person(name: 'Marvin', age: 42)).go(context);
$extra 会在位置之外被传递,会使动态链接和深度链接失败(包括浏览器的返回按钮),也不建议用于目标为Flutter Web 的应用。
混合参数
当然,也可以联合使用路径、查询和$extra 参数:
class HotdogRouteWithEverything extends GoRouteData {
HotdogRouteWithEverything(this.ketchup, this.mustard, this.$extra);
final bool ketchup; // required path parameter 必需的路径参数
final String? mustard; // optional query parameter 可选的查询参数
final Sauce $extra; // special $extra parameter 特殊的附加参数
@override
Widget build(BuildContext context) => HotdogScreen(ketchup, mustard, $extra);
}
这看上去有点儿愚蠢,但是可以正常使用。
重定向
重定向使用代码生成器提供的路由的 location 属性:
redirect: (state) {
final loggedIn = loginInfo.loggedIn;
final loggingIn = state.subloc == LoginRoute().location;
if( !loggedIn && !loggingIn ) return LoginRoute(from: state.subloc).location;
if( loggedIn && loggingIn ) return HomeRoute().location;
return null;
}
路由级别重定向
处理路由级别重定向通过实现路由的 redirect 方法:
class HomeRoute extends GoRouteData {
// no need to implement [build] when this [redirect] is unconditional
// 当此[redirect] 无条件时,无需实现 [build]
@override
String? redirect() => BooksRoute().location;
}
类型转换
代码生成器可以将简单类型如 int 和 enum , 与底层变量的字符串类型进行相互转换。
enum BookKind { all, popular, recent }
class BooksRoute extends GoRouteData {
BooksRoute({this.kind = BookKind.popular});
final BookKind kind;
@override
Widget build(BuildContext context) => BooksScreen(kind: kind);
}
过渡
默认情况下, GoRouter 使用在组件树中找到的 APP,例如: MaterialApp 、
CupertinoApp 、 WidgetApp 等,使用相应的页面类型来创建包装路由的 build 方法 返回的 Widget 的页面,例如:MaterialPage 、 CupertinoPage 、 NoTransitionPage 等。此外,它会使用 state.pageKey 属性来设置页面的 key 属性 和 页面的 restorationId 。
过渡覆写
如果想要改变页面如何创建,例如,使用一个不同的页面类型,当创建页面时传递一个非默认参数(如一个自定义 key ) 或者访问 GoRouterState 对象,可以覆写基类的 buildPage 方法来代替 build 方法:
class MyMaterialRouteWithKey extends GoRouteData {
static final _key = LocalKey('my-route-with-key');
@override
MaterialPage<void> buildPage(BuildContext context, GoRouterState state) =>
MaterialPage<void>(
key: _key,
child: MyPage(),
);
}
自定义过渡
覆写 buildPage 方法也可用于自定义过渡:
class FancyRoute extends GoRouteData {
@override
MaterialPage<void> buildPage(BuildContext context, GoRouterState state) =>
CustomTransitionPage<void>(
key: state.pageKey,
child: FancyPage(),
transitionsBuilder: (context, animation, animation2, child) =>
RotationTransition(turns: animation, child: child),
),
}
实现的注意事项
@TypedGoRoute注解:- 它只用于注解
GoRouterData的实现。 - 必须提供类型参数。
- 根
@TypedGoRoute的类型参数必须匹配注解的类。 - 以后,可能会:
- 扩展可注解的集合,组件?库?
- 它只用于注解
GoRouteData实现:- 必须有一个默认的构造方法(或工厂方法)
- 所有的构造方法的参数必须映射1对1的名称和类型,类型为成员或 getter 的类型。
- 构造方法的参数只有对齐 getter 的字段。
- 可以有一个
const的构造方法。对于没有参数的构造方法,const会用来初始化类型。 - 有效的 参数/成员/属性类型:
String: 原样表示num、int、bool、DateTime、Duration、BigInt、double、Uri: 使用toString编码,使用解析器或类型的东西解码。Enum: 使用 kebab 大小写名称 进行编码/解码。
- 以后,可能会:
- 支持可写的成员或 setter。