Flutter 基于文件的路由
我受到 remix.run 中的路由(包括嵌套布局和服务器端组件)的启发,所以我决定在 flutter 中进行实验。
由于这需要在编译时完成,我编写了一个生成器来解析 pages 目录以获取基于文件的路由名称,以定义类似 regex_router 的正则表达式。
安装
您需要在本地安装 dart,然后可以在项目目录中运行以下命令:
dart generator/bin/main.dart
这将生成一个 generated.g.dart 文件,可用于导入生成的 widget 以运行应用程序。
import 'package:flutter/material.dart';
import 'generated.g.dart';
void main() {
runApp(GeneratedApp(
themeMode: ThemeMode.system,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
));
}
我还包含了一个生成器所需的 router.dart 文件以及所有本地 widget。
定义基础布局
您可以使用根名称定义基础布局。例如:about.dart
import 'package:flutter/material.dart';
import '../generated.g.dart';
class AboutPage extends UiRoute<void> {
@override
Widget builder(BuildContext context, void data, Widget? child) {
return Scaffold(
appBar: AppBar(
title: const Text('About'),
),
body: child,
);
}
}
如果您注意到,子项可以为 null,并用于嵌套布局。
这不是一个 widget,而是一个我们可以选择性加载数据的类。
定义索引路由
当不需要参数时,您可以为索引路由定义索引路由。例如:about/index.dart
import 'package:flutter/material.dart';
import '../../generated.g.dart';
class AboutDetails extends UiRoute<void> {
@override
Widget builder(BuildContext context, void data, Widget? child) {
return const Center(
child: Text('About'),
);
}
}
由于这是一个嵌套布局,您只需提供组件即可,它将继承自父布局 (about.dart)。
定义命名参数
如果某个路由不需要获取数据,您可以为其定义一个命名参数。例如:/about/guest.dart
import 'package:flutter/material.dart';
import '../../generated.g.dart';
class GuestPage extends UiRoute<void> {
@override
Widget builder(BuildContext context, void data, Widget? child) {
return const Center(
child: Text('Guest'),
);
}
}
这也只是一个组件。
定义动态参数
有时参数是在运行时生成的,或者需要从数据库中提取。例如:about/:id.dart
import 'package:flutter/material.dart';
import '../../generated.g.dart';
class AccountPage extends UiRoute<Map<String, String>> {
@override
loader(route, args) => args;
@override
Widget builder(
BuildContext context, Map<String, String> data, Widget? child) {
return Center(
child: Text('ID: ${data['id']}'),
);
}
}
您可以看到我们使用冒号 (:) 作为前缀来定义要查找和匹配的参数。这将以 map 的形式提供。
加载器可用于从数据库中提取数据,但在本例中,它返回参数 map。默认情况下,它返回 null。
加载器在 widget 构建之前运行。
路由
要导航到另一个页面,您需要分发以下事件,而不是使用 Navigator.of(context)
RoutingRequest('ROUTE_HERE').dispatch(context)
ROUTE_HERE 应该是您的路由名称,例如 /about/30。
存储状态
一切都应该是无状态的,但在示例中您可以看到,即使是底部标签导航的索引也可以仅通过路由来完成。
结论
这解决了各种布局问题,可以提供漂亮的 URL,同时只加载一次数据并在需要时进行缓存。