助力Flutter自动化,那些配合 build_runner 使用的常用插件
前言
现在公司 Flutter 项目多了,后面还有大型项目需要用 Flutter 重构,目前的状态是手上多个 Flutter 项目开发与维护,属于是 Getx 与 Bloc 双修了。
但是 Bloc 好用是好用,但是模板代码太多了,就想着使用 build_runner 统一管理项目的模板代码生成,方便快速开发,经历聚焦到业务代码上 o( ̄ヘ ̄o#)
为什么想到会使用 build_runner 这种插件呢?
因为目前的大型项目都是使用组件化开发了,我之前是使用 Asset 工具自动生成的,是一个 AS 的插件,例如生成资源的,Generate Asset 和 生成 Json 对象的 FlutterBeanFactory ,但是这种插件生成只会在宿主中执行,我们的子组件中根本拿不到宿主的资源。所以这种 AS 插件就不太方便了,到头来还是用 build_runner 这种方案手动的在组件内部执行。
由于 build_runner 可以配合的插件太多了,对于常规应用和 Bloc 应用来说,有以下几种常用的插件,一起看看如何集成与使用吧。
下面会给出详细的代码与图文。
一、flutter_gen_runner 资源生成
为你的项目自动生成颜色,字体,图片,SVG等资源,仓库地址为:pub.dev/packages/fl...
yaml 中集成相关配置
yaml
...
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# https://pub.dev/packages/build_runner
build_runner: ^2.4.9
# https://pub.dev/packages/flutter_gen
flutter_gen_runner: ^5.4.0
flutter_gen:
output: lib/generated/
line_length: 160
null_safety: true
integrations:
flutter_svg: true
assets:
enabled: true
fonts:
enabled: true
colors:
inputs:
- assets/color/colors.xml
flutter:
uses-material-design: true
generate: false
assets:
- assets/images/
- assets/images/base_lib/
- assets/images/base_service/
- assets/images/cpt_auth/
- assets/images/cpt_profile/
fonts:
- family: Raleway
fonts:
- asset: assets/fonts/Raleway-Regular.ttf
- asset: assets/fonts/Raleway-Italic.ttf
style: italic
我们只需要在 dev_dependencies 中集成,只是在开发阶段用于生成对应的代码。
flutter_gen 是它对应的配置,主要的配置是 output 用于指定生成文件的存放路径,colors 用于指定颜色资源的路径。
在命令行执行
arduino
dart run build_runner build
或者
dart run build_runner watch
一个是一次执行,一个是监视文件。执行之后就会在对应的 output 生成对应的文件
注意这里是组件化开发,需要切换到对应组件中执行命令哦,如果你就是在app中开发,那直接在根目录执行即可。
我们就能看到我们生成的资源类了。
scss
MyAssetImage(
Assets.images.baseLib.dialogDeleteIcon.path,
width: 26,
height: 26,
).backgroundColor(ColorName.gray410)
我们就可以使用图片与颜色资源啦。
二、injectable_generator 依赖注入
说到 injectable 就得知道 get_it 插件,这是一个依赖注入插件,和 Getx 中的依赖注入功能比较类似。虽然都有"get"这个单词,但是两者没有关系。
injectable 是通过使用注解来自动注册类为依赖项,injectable_generator 是与之配套使用的代码生成器,它可以读取你的注解并自动生成依赖注入的代码,build_runner 是执行代码生成任务的工具。
关于 get_it 如何使用,基于什么原理实现的?这不是本文的重点,有兴趣可以单独开一篇。我们先看看如何使用
yaml
dependencies:
flutter:
sdk: flutter
injectable: ^2.4.1
dev_dependencies:
flutter_test:
sdk: flutter
injectable_generator: ^2.6.1
build_runner: ^2.4.9
咦?你为什么没有集成 get_it 插件?我们集成了 injectable 插件会自带 get_it 插件的。
使用,先定义
csharp
class MyService{
void sayHello(){
print("Hello MyService");
}
}
注入和取出:
scss
getIt.registerFactory(() => MyService());
getIt.registerSingleton(() => MyService());
getIt.registerLazySingleton(() => MyService());
getIt<MyService>().sayHello();
getIt.get<MyService>().sayHello();
GetIt.I.get<MyService>().sayHello();
getIt.unregister();
用了 injectable 之后,先创建初始化代码
dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart';
final getIt = GetIt.instance;
@InjectableInit(
initializerName: 'init', // default
preferRelativeImports: true, // default
asExtension: true, // default
)
void configureDependencies() => getIt.init();
普通的Factory注入:
typescript
import 'package:injectable/injectable.dart';
@injectable
class MyService{
void sayHello(){
print("Hello MyService");
}
}
单例类注入:
typescript
import 'package:injectable/injectable.dart';
@singleton
class MySingleService{
void sayHello(){
print("Hello MySingleService");
}
}
可以通过构造注入,像 Android Hilt 一样会自动查找
kotlin
import 'package:injectable/injectable.dart';
import 'my_service.dart';
import 'my_single_service.dart';
@injectable
class MyController {
final MyService myService;
final MySingleService mySingleService;
MyController(this.myService, this.mySingleService);
void print() {
myService.sayHello();
mySingleService.sayHello();
}
}
运行命令 dart run build_runner build
得到生成的文件如下:
markdown
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// InjectableConfigGenerator
// **************************************************************************
// ignore_for_file: type=lint
// coverage:ignore-file
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:get_it/get_it.dart' as _i1;
import 'package:injectable/injectable.dart' as _i2;
import '../service/my_controller.dart' as _i5;
import '../service/my_service.dart' as _i3;
import '../service/my_single_service.dart' as _i4;
extension GetItInjectableX on _i1.GetIt {
// initializes the registration of main-scope dependencies inside of GetIt
_i1.GetIt init({
String? environment,
_i2.EnvironmentFilter? environmentFilter,
}) {
final gh = _i2.GetItHelper(
this,
environment,
environmentFilter,
);
gh.factory<_i3.MyService>(() => _i3.MyService());
gh.singleton<_i4.MySingleService>(() => _i4.MySingleService());
gh.factory<_i5.MyController>(() => _i5.MyController(
gh<_i3.MyService>(),
gh<_i4.MySingleService>(),
));
return this;
}
}
使用的时候就是和默认的GetIt一样的用法了。
ini
MyController controller = GetIt.I.get<MyController>();
print("controller对象:$controller 调用内部的方法==>");
controller.print();
MySingleService singleService = GetIt.I.get<MySingleService>();
print("singleService对象:$singleService 调用内部的方法==>");
singleService.sayHello();
打印结果如下:
添加生命周期:
typescript
import 'package:injectable/injectable.dart';
@singleton
class MySingleService{
void sayHello(){
print("Hello MySingleService");
}
@PostConstruct()
void init() {
print("MySingleService -- init()");
}
@disposeMethod
void dispose(){
print("MySingleService -- dispose()");
}
}
@injectable
class MyController {
final MyService myService;
final MySingleService mySingleService;
MyController(this.myService, this.mySingleService);
void printHello() {
myService.sayHello();
mySingleService.sayHello();
}
@PostConstruct()
void init() {
print("MyController -- init()");
}
}
使用的时候:
ini
MyController controller = GetIt.I.get<MyController>();
print("controller对象:$controller 调用内部的方法==>");
controller.printHello();
MySingleService singleService = GetIt.I.get<MySingleService>();
print("singleService对象:$singleService 调用内部的方法==>");
singleService.sayHello();
Future.delayed(Duration(seconds: 5)).then((value){
GetIt.I.unregister<MySingleService>();
});
可以看到创建的时候会走 init 方法,销毁的时候会走 dispose 方法:
总的来说使用起来还是很平滑的,并不复杂。并且对于依赖注入理解的话也很容易上手,特别是 Android 开发的小伙伴们对 Hilt 这个框架有了解的,可以无缝衔接。
不使用 injectable 时:
所有依赖项的注册必须手动编写和管理。
更灵活但更易出错,尤其是在大型项目中。
使用 injectable 时:
注册通过注解自动化,代码更少,容易维护。 初始化时自动生成代码,减少了人为错误。
额外话题:
使用 injectable 与 Bloc 框架更加搭配哦!例如我们可以标记一个 Bloc 类注入进去
scala
@Injectable()
class HomeBloc extends BaseBloc<HomeEvent, HomeState> {
HomeBloc(this._repository) : super(HomeState()) {
...
}
final ArticleRepository _repository;
}
我们的数据仓库本身是需要设置为单例的,然后我们的 Bloc 设置为非单例,在 HomePage 中通过 GetIt 找到并设置给 BlcoProvide ,并且 Bloc 和 GetIt 都是当前页面生效的,当页面关闭,对象不再持有都是会自动回收的,完美配合。
less
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
HomeBloc homeBloc = GetIt.I.get<HomeBloc>();
@override
Widget build(BuildContext context) {
// 使用BlocProvider来提供HomePageBloc
return BlocProvider<HomePageBloc>(
create: (context) => homeBloc,
child: Scaffold(
appBar: AppBar(
title: const Text('Home Page'),
),
body: BlocBuilder<HomePageBloc, HomePageState>(
builder: (context, state) {
// 这里可以根据HomePageState来构建你的UI
// 例如,可以使用state.index来决定显示哪个页面或者内容
return Center(
child: Text('当前索引是:${state.index}'),
);
},
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: context.select((HomePageBloc bloc) => bloc.state.index),
items: const [
BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Home'),
BottomNavigationBarItem(icon: Icon(Icons.business), label: 'Business'),
// 更多的项目...
],
onTap: (index) {
// 当用户点击BottomNavigationBar时,我们向Bloc发送一个HomePageDots事件
context.read<HomePageBloc>().add(HomePageDots(index));
},
),
),
);
}
}
甚至还能通过泛型进行一些封装,这里就不展开了,期待我后续。
三、json_annotation Json序列化与反序列化
这个是大家的老朋友了,简单介绍一下。
json_serializable 和 json_annotation 是 Dart 语言中用来处理 JSON 数据序列化和反序列化的库,通常在使用 Flutter 开发时会用到。要使用这些库,你需要将它们添加到你的pubspec.yaml文件中,并在你的 Dart 类中使用注解。
配合 build_runner 插件使用可以自动生成Json格式的序列化与反序列化。
yaml
dependencies:
flutter:
sdk: flutter
json_annotation: ^4.9.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.9
json_serializable: ^6.8.0
第二步我们需要创建自己的模型
dart
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart'; // 1. 这个 part 声明是必须的,它指向生成的文件
@JsonSerializable() // 2. 这个注解告诉 json_serializable 包要为这个类生成代码
class User {
final String name;
final String email;
User({required this.name, required this.email});
// 3. 工厂构造函数,从 JSON 创建一个新的实例
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
// 4. `toJson` 是一个方法,将 User 实例转化为一个 map
Map<String, dynamic> toJson() => _$UserToJson(this);
}
如何从Json格式转换为这个模型呢,网上有很多的推荐,这里给出一个网站转换工具【传送门】。
让我们执行命令 dart run build_runner build
就可以看到已经生成成功了:
这算是比较基础的使用了,当然了很多开发者是不用这种方案的,有些人是手撕,有些人是用 AS 插件,八仙过海各显神通。所以我也只是简单的介绍一下。
四、freezed 快速创建不可变对象
使用 json_serializable、json_annotation 来自动创建对象的序列化与反序列化已经是很方便了,但是还是会感觉到很复杂,有没有更简单的方法。
我们知道 Flutter 中定义一个 Model 很冗长,我们需要定义构造方法 + 属性,还要覆写 toString、 操作符 ==、 hashCode 等方法,如果是一些状态管理 State 我们还需要实现一个 copyWith 方法用来克隆对象。
最麻烦的就是处理序列化/反序列化。要实现所有这些代码,可能需要几百行代码,这很容易导致错误并明显地影响 Model 的可读性。
但是freezed 来了!这些工作我们都可以交给 freezed 去自动生成,尤其是 freezed 是支持 json_serializable、json_annotation 的,可以简化序列化与反序列化的过程。
怎么使用呢?
我们在yaml中加入对应的依赖:
yaml
# https://pub.dev/packages/injectable
injectable: ^2.4.1
# https://pub.dev/packages/freezed_annotation
freezed_annotation: ^2.4.1
json_annotation: ^4.9.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# https://pub.dev/packages/build_runner
build_runner: ^2.4.9
# https://pub.dev/packages/injectable
injectable_generator: ^2.6.1
# https://pub.dev/packages/freezed
freezed: ^2.5.1
# https://pub.dev/packages/flutter_gen
flutter_gen_runner: ^5.4.0
# https://pub.dev/packages/json_annotation
json_serializable: ^6.8.0
我们先直接定义相对的模板:
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'profile.freezed.dart';
part 'profile.g.dart';
@freezed
class Profile with _$Profile {
const factory Profile({
required String id,
required String name,
required String avatar,
}) = _Profile;
factory Profile.fromJson(Map<String, dynamic> json) => _$ProfileFromJson(json);
}
然后执行命令 dart run build_runner build
就能生成对应的代码啦:
如果我们的对象不想要序列化功能,我们就能去掉对应的 fromJosn 方法即可:
scala
import 'package:freezed_annotation/freezed_annotation.dart';
part 'id_name.freezed.dart';
@freezed
class IdNameBean with _$IdNameBean {
const factory IdNameBean({
String? id,
String? name,
}) = _IdNameBean;
}
这样就只会生成 id_name.freezed.dart 文件了,这样就是不带序列化的对象了。
**为什么要用 freezed 呢?**因为 Bloc 中我们需要用到大量的 copywith ,状态的刷新,状态的对比等等,如果每一个 State 都自己手写,就太多的重复代码了,工作量太大。使用 freezed 就会很方便了。
你还不是要手写 freezed 类的模板?哪里简单了?
确实是要重复写这些模板,不过我们可以通过 Live Templates 模板来超级简化相关的流程。
比如我们创建对应的类文件之后,直接用模板生成代码,给它一个类名。
scala
import 'package:freezed_annotation/freezed_annotation.dart';
@freezed
class $NAME$ with _$$$NAME$ {
const factory $NAME$() = _$NAME$;
factory $NAME$.fromJson(Map<String, dynamic> json) => _$$$NAME$FromJson(json);
}
然后指定 part 引入的资源
这里都是一些固定的模板套路,只需要定义类名即可。
dart
part '$FILENAME$.freezed.dart';
part '$FILENAME$.g.dart';
注意如果不需要序列化功能,可以去掉 fromJson 方法和对应的part g.dart 依赖。
然后我们指定对应的参数
scala
@freezed
class IdNameBean with _$IdNameBean {
const factory IdNameBean({
String? id,
String? name,
}) = _IdNameBean;
}
执行命令就能生成对应的代码啦,是不是很方便呢?
此时配合 Bloc 框架我们就能很方便的定义对应的 Event 和 State 啦!
home_event.dart:
dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'home_event.freezed.dart';
abstract class HomeEvent extends BaseBlocEvent {
const HomeEvent();
}
@freezed
class HomePageInitiated extends HomeEvent with _$HomePageInitiated {
const factory HomePageInitiated() = _HomePageInitiated;
}
home_state.dart:
java
import 'package:freezed_annotation/freezed_annotation.dart';
part 'home_state.freezed.dart';
@freezed
class HomeState extends BaseBlocState with _$HomeState {
factory HomeState({
@Default([]) List<PlayListItemData> playList,
}) = _HomeState;
}
方便!
五、auto_route 自动创建路由
我们使用原生的 Navigation 过于复杂,使用 AutoRoute 这样的高级路由库,可以让路由管理变得更加简洁和高效。在 AutoRoute 中,您不需要手动管理路由栈,因为库为您抽象了这一层的细节。AutoRoute 提供了一种声明式的方式来定义路由,使得路由之间的关系更加清晰,并且可以更容易地控制页面的导航和状态。
我们实现对应的注解和注解处理器,添加依赖如下
yaml
# https://pub.dev/packages/auto_route
auto_route: ^8.0.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
# https://pub.dev/packages/build_runner
build_runner: ^2.4.9
# https://pub.dev/packages/injectable
auto_route_generator: ^8.0.0
# https://pub.dev/packages/freezed
使用步骤:
第一步 先定义我们的配置入口
scala
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
part 'app_router.gr.dart';
@AutoRouterConfig(replaceInRouteName: 'Page|Screen,PageRoute')
class AppRouter extends _$AppRouter {
@override
List<AutoRoute> get routes => [
];
}
这里我指定生成的路由信息类为 PageRouter 格式,个人喜好问题,你完全可以自定义,或者使用默认的。
第二步 在我们的页面加上注解
scala
@RoutePage()
class SecondPage extends StatefulWidget {
const SecondPage({super.key, required this.articleId});
final String? articleId;
@override
State<SecondPage> createState() => _SecondPageState();
}
...
第三步 运行命令生成代码 dart run build_runner build
此时就生成了文件啦:
生成的文件很长,页面越多越长。
第四步 把生成的信息类配置起来
dart
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import '../page/first_page.dart';
import '../page/second_page.dart';
import '../page/test_appbar_list_page.dart';
import '../page/test_appbar_list_page2.dart';
part 'app_router.gr.dart';
@AutoRouterConfig(replaceInRouteName: 'Page|Screen,PageRoute')
class AppRouter extends _$AppRouter {
@override
List<AutoRoute> get routes => [
AutoRoute(page: FirstPageRoute.page, initial: true),
AutoRoute(page: SecondPageRoute.page, path: '/second'),
AutoRoute(page: TestAppbarListViewPageRoute.page),
AutoRoute(page: TestAppbarListViewPageRoute2.page),
];
}
注意看生成的类名 SecondPageRoute.page
是由 SecondPage 再根据我们的配置 @AutoRouterConfig(replaceInRouteName: 'Page|Screen,PageRoute')
指定生成的类名。
第五步 入口函数中配置起来
scala
class MyApp extends StatelessWidget {
final _rootRouter = AppRouter();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: _rootRouter.config(),
);
}
}
使用:
简单的跳转,返回,与查看当前栈顶,查找全部栈。
scss
ElevatedButton(
onPressed: () {
context.router.push(TestAppbarListViewPageRoute2());
},
child: Text('Go to List Page'),
),
ElevatedButton(
onPressed: () {
context.router.maybePop();
},
child: Text('Back to First Page'),
),
ElevatedButton(
onPressed: () {
RouteData? routeData = context.router.currentChild;
print("current routeData:${routeData?.name}");
List<AutoRoutePage> list = context.router.stack;
print("AutoRoutePageList:${list.toString()}");
},
child: Text('查看当前的路由栈'),
),
由于 AutoRouter 帮我们维护了路由栈,由此我们可以展开很多骚操作了,这里就不展开了,有兴趣可以看看我之前的文章。
其他常用的 API 为:
csharp
router.push(const BooksListRoute());
router.replace(const BooksListRoute());
router.navigate(const BooksListRoute());
context.router.maybePop();
context.router.popUntilRoot();
context.router.popUntilRouteWithName('HomeRoute');
context.router.removeLast()
当然还有很多其他的用法,比如嵌套导航,Tab导航,声明式导航,还有很多高级用法,如路由控制,拦截与重定向,路由守护,路由观察等等,由于这一期也不是路由专题就先略过了,我们先知道怎么基本的使用即可。
使用注解生成路由这种方法的好处是:
维护性:当你的应用的导航结构变化时,你只需要更新注解,然后重新生成代码即可。
简洁性:你不需要手动编写和维护大量的路由代码。
安全性:通过生成的代码,可以在编译时捕获更多的错误,而不是在运行时。
PS:当然有很多人是用其他路由的,比如 Fluro、Frouter,go route,或者 route master等等,或者干脆就是用原生路由或封装原生路由也不是不行,方法很多,真的是百花齐放,还是那句话,这期不是路由专题不要偏题,这里先不展开讨论了。
后记
除了这些插件我们还有茫茫多的插件可以配合 build_runner 使用,本文只列出了这些是应用是自己平常项目中用到的,觉得比较常用的。其他的类似 mobx_codegen ,retrofit_generator,functional_widget_annotation,build_value_generator,intl_utils 等等,有需要可以自行添加哦。
我了解一些开发者是很不喜欢使用 build_runner 的,就和使用 Getx 框架一样有些争议,关于这一点我也说明一下,这些并不是必须使用的,完全可以手撕。
- 优点:
自动化和一致性:代码生成器可以自动创建模板化的代码块,确保项目中的相似模块具有一致的结构和实现方式。
减少样板代码:对于重复性的代码模式,比如 JSON 序列化和反序列化,使用注释和代码生成可以减少大量手写的样板代码。
类型安全:生成的代码是类型安全的,这可以减少运行时错误,并且在编译时提供错误检查。
易于重构:由于代码是自动生成的,重构或更新代码模式时只需要更改生成器逻辑或注释,并重新生成代码即可。
提升开发效率:自动化代码生成可以加快开发流程,允许开发者更专注于业务逻辑和核心功能。
可维护性:生成的代码通常会遵循一定的规范和模式,这使得长期维护项目变得更容易。
- 缺点:
学习曲线:对于新手开发者来说,理解和使用注释以及 build_runner 可能会需要一段时间。
构建时间增加:使用代码生成器意味着每次更改注释后都需要运行构建过程,这可能会增加项目的构建时间。
调试困难:如果生成的代码出现了问题,调试可能会比较复杂,因为开发者需要理解生成器是如何从注释创建代码的。
依赖管理:项目依赖可能会增加,因为每个代码生成器都是一个单独的包,需要维护和更新。
版本兼容性:升级项目的依赖时可能会遇到代码生成器的兼容性问题,特别是在大型项目或者多个生成器共同工作时。
间接性:有时候自动生成的代码会引入一个间接层次,使得从编写的注释到最终的代码实现之间存在一定的距离。
过度依赖:过度依赖代码生成可能会导致开发者忽视更简单或更合适的解决方案。
对于我个人来说我都能接受,毕竟 Getx 我也用过,虽然现在转到 Bloc 了,但是我对 build_runner 这种生成模板代码的方案我是能接受的,除非有更好的更方便更快捷的方案。
我主要难以忍受的就是编译时间太慢了,修改添加一个类等半天,没有 AS 的插件好用,但是组件化开发中中 AS 的插件也没有那么的好用了,纠结中...
文章篇幅很长,终于来到了尾声,本人其实开发Flutter项目的时间并不长,并不是资深Flutter开发者,文章难免有错误,思路难免会走弯,如果你有其他的更多的更好的实现方式,也希望大家能评论区交流一起学习进步。如果我的文章有错别字,不通顺的,或者代码、注释、有错漏的地方,同学们都可以指出修正。
了解了 build_runner 自动生成模板代码之后,我后期会出一篇文章讲讲我是如何转到 Bloc 的,又如何快速理解 Bloc 概念。
由于不是具体的 Demo ,本文代码都已在文中贴出作为参考,如果感觉本文对你有一点的启发和帮助,还望你能点赞
支持一下,你的支持对我真的很重要。
Ok,这一期就此完结。