Flutter反射机制增强插件reflect_buddy的使用
Flutter反射机制增强插件reflect_buddy的使用
基于反射(dart:mirrors)的强大的实时Dart JSON序列化/反序列化器
简介
如果你之前从事过C#开发,你可能会喜欢它能够轻松地将普通对象序列化和反序列化的能力。无需准备任何模型,并且可以通过属性来管理字段。为了编写一个易于使用的后端解决方案,我在Dart中非常想念这种功能。因此,我决定自己开发它。欢迎了解 Reflect Buddy:
概念
此库用于基于JSON输入生成严格类型的对象,而无需提前准备任何模型。它在运行时工作,实际上是在动态生成。
该库可以序列化和反序列化具有任意深度嵌套的对象。
大多数Dart中的序列化器都是使用代码构建器编写的。这是因为它们通常与Flutter一起使用,而Flutter的发布版本使用所谓的AOT(提前编译)。AOT编译使得在运行时无法组装类型。所有类型都是预先已知的。
与其它序列化器不同,Reflect Buddy 使用JIT(即时编译),并且不需要任何预构建的模型。几乎任何常规类都可以通过调用一个方法进行序列化/反序列化。
背景
该工具最初作为我的另一个项目:Dart Net Core API 的组件开发。该项目部分受到C#库 Dotnet Core API 的启发。但由于它可能对其他开发有用,我决定将其放在一个单独的包中。
工作原理
假设你有一个类,例如一个包含典型值如名字、姓氏、年龄、ID等的用户类。你想通过网络发送用户的实例。当然,你需要将其序列化为某种简单的数据,比如JSON字符串。
通常,你需要手动编写 toJson()
方法或使用像 json_serializable
这样的代码生成模板。这是个非常好的选项,如果你使用的是AOT编译。但在JIT中,你可以通过简单地调用 toJson()
方法来极大地简化它。就是这样,真的那么简单。
一个像这样的类完全准备好与 Reflect Buddy 一起工作。正如你所见,这里没有任何特殊的东西。只是一个普通的Dart类。
class User {
String? firstName;
String? lastName;
int age = 0;
DateTime? dateOfBirth;
}
你可以使用可空或非可空字段。你也可以使用 late
修饰符。但要小心,因为如果你在你的json中不提供字段的值,它会在运行时失败。所以我建议使用默认值或可空类型。但这取决于你自己。
限制
- Reflect Buddy 可以处理基本结构:
Map
和List
。它不能处理更复杂的类型,如Set
,所以你需要根据JSON来规划你的类。 - Reflect Buddy 只能与JIT编译一起使用。所以它不会在Flutter中工作,不要尝试这样做。对于Flutter,我推荐使用
json_serializable
。
支持的内置类型
Map
+ 泛型List
+ 泛型DateTime
+ 可以使用@JsonDateConverter(dateFormat: 'yyyy_MM_dd')
来指定自定义格式。这在两个方向上都有效。你可以在示例中看到它。String
double
int
num
bool
Enum
开始使用
导入库
import 'package:reflect_buddy/reflect_buddy.dart';
获取你想要反序列化的JSON,例如
const containerWithUsers = {
'id': 'userId123',
'users': {
'male': {
'firstName': 'Konstantin',
'lastName': 'Serov',
'age': 36,
'dateOfBirth': '2018-01-01T21:50:45.241520'
},
'female': {
'firstName': 'Karolina',
'lastName': 'Serova',
'age': 5,
'dateOfBirth': '2018-01-01T21:50:45.241520'
},
}
};
以及你想要反序列化的类型。它们必须完全对应你的JSON结构
class ContainerWithCustomUsers {
String? id;
Map<String, User>? users;
}
class User {
String? firstName;
String? lastName;
int age = 0;
DateTime? dateOfBirth;
Gender? gender;
}
enum Gender {
male,
female,
}
有三种方法可以从JSON创建 ContainerWithCustomUsers
实例。它们在内部使用相同的逻辑。所以只需选择你喜欢的方法。
- 直接在类型上调用
fromJson
方法(这里需要括号来区分这个调用和静态方法调用)
final containerInstance = (ContainerWithCustomUsers).fromJson(containerWithUsers);
- 使用泛型简写方法
final containerInstance = fromJson<ContainerWithCustomUsers>(containerWithUsers);
- 对象扩展方法
final containerInstance = containerWithUsers.toInstance<ContainerWithCustomUsers>();
类的序列化和反序列化
有时处理JSON需要隐藏或添加某些键到输出中。
例如,在你的User模型中,_id
是一个私有字段,但你想将其返回给前端以便唯一标识对象。
默认情况下,Reflect Buddy 忽略私有字段,但你可以通过向字段添加 @JsonInclude()
注解来强制其进入json
class SimpleUserWithPrivateId {
@JsonInclude()
String? _id;
String? firstName;
String? lastName;
int age = 0;
Gender? gender;
DateTime? dateOfBirth;
}
/// 这也会包括私有字段 `_id`
void _processSimpleUserWithPrivateId() {
final instance = fromJson<SimpleUserWithPrivateId>({
'_id': 'userId888',
'firstName': 'Konstantin',
'lastName': 'Serov',
'age': 36,
'gender': 'male',
'dateOfBirth': '1987-01-02T21:50:45.241520'
});
print(instance);
final json = instance?.toJson();
print(json);
}
使用注解
上面已经有一个使用注解的例子。@JsonInclude()
的例子。该库还有几种内置注解。其中一个就是 @JsonIgnore()
。你可以添加到任何字段以排除其输出。之后,即使模型中填充了该字段,该字段也不会包含在JSON中。
class SimpleUser {
/// 这将从结果JSON中排除 firstName 字段
@JsonIgnore()
String? firstName;
String? lastName;
int age = 0;
Gender? gender;
DateTime? dateOfBirth;
}
注意:默认情况下,toJson()
方法只序列化一个级别的实例。这意味着它忽略了任何超类字段。但在某些情况下,你也想包含父类字段。例如,id
、createdAt
、updatedAt
是所有模型的常见字段。没有必要在每个模型中写入它们。只需在超类中声明它们,并在子类中添加 @JsonIncludeParentFields()
注解。
class BaseModel {
int? id;
DateTime? createdAt;
DateTime? updatedAt;
}
@JsonIncludeParentFields()
class User extends BaseModel {
int? age;
String? firstName;
String? lastName;
}
还可以通过设置全局 alwaysIncludeParentFields
为 true
来全局设置,但要小心使用此功能,因为它可能会在不需要时暴露一些敏感信息。最好在每个需要它的类上使用 @JsonIncludeParentFields
。
如果你仍然希望全局使用但它想为某些特定类隐藏父字段,可以将 alwaysIncludeParentFields
设置为 true
,但在该类上使用 @JsonExcludeParentFields()
。
验证器
经常需要在赋值前验证值。你可以为此目的使用 JsonValueValidator
的后代。这是一个抽象类,只有一个名为 validate
的方法。该方法由 Reflect Buddy 内部调用,并接受两个参数:即将分配给字段的实际值和字段名(用于日志记录目的)。
你可以扩展 JsonValueValidator
类并为任何字段编写自己的值验证逻辑。如果值无效,只需抛出异常。
以下是 NumValidator
继承者的示例
class NumValidator extends JsonValueValidator {
const NumValidator({
required this.minValue,
required this.maxValue,
required super.canBeNull,
});
final num minValue;
final num maxValue;
@override
void validate({
num? actualValue,
required String fieldName,
}) {
if (checkForNull(
canBeNull: canBeNull,
fieldName: fieldName,
actualValue: actualValue,
)) {
if (actualValue! < minValue || actualValue > maxValue) {
throw Exception(
'"$actualValue" is out of scope for "$fieldName" expected ($minValue - $maxValue)',
);
}
}
}
}
值转换器
另一种可能的注解用途是数据转换。想象一下,你不希望抛出超出范围的值异常,但也不想分配无效值。在这种情况下,你可以使用 JsonValueConverter
的后代。
就像 JsonValueValidator
一样,你可以扩展 JsonValueConverter
并为 Object? convert(covariant Object? value);
方法编写自己的实现。
JsonDateConverter
的实现示例
class JsonDateConverter extends JsonValueConverter {
const JsonDateConverter({
required this.dateFormat,
});
final String dateFormat;
@override
Object? convert(covariant Object? value) {
if (value is String) {
return DateFormat(dateFormat).parse(value);
} else if (value is DateTime) {
return DateFormat(dateFormat).format(value);
}
return null;
}
}
在 JsonDateConverter
中,你可以传递任何日期格式,并将其用于从或到字符串解析日期。例如:
@JsonDateConverter(dateFormat: 'yyyy-MM-dd')
这将允许你在JSON中拥有自定义日期表示形式
或者这个。它只是将数值钳位
class JsonNumConverter extends JsonValueConverter {
const JsonNumConverter({
required this.minValue,
required this.maxValue,
required this.canBeNull,
});
final num minValue;
final num maxValue;
final bool canBeNull;
@override
num? convert(covariant num? value) {
if (value == null) {
if (canBeNull) {
return value;
}
return minValue;
}
return value.clamp(minValue, maxValue);
}
}
键名称转换器
有些场景需要更改输出JSON中的键名称。例如,在数据库中存储为 camelCase
,但在前端上需要 snake_case
(或其他)。Reflect Buddy 也有专门的注解来处理这种情况。它们继承自 JsonKeyNameConverter
。这是一个使用多个不同转换器的示例。
class SimpleUserKeyConversion {
@CamelToSnake()
String? firstName;
@CamelToSnake()
String? lastName;
@FirstToUpper()
int age = 0;
@FirstToUpper()
Gender? gender;
@FirstToUpper()
DateTime? dateOfBirth;
}
调用该类实例的 toJson()
将导致以下结果:
{first_name: Konstantin, last_name: Serov, Age: 36, Gender: male, DateOfBirth: 1987-01-02T21:50:45.241520}
当然,如果你对所有字段使用相同的键命名策略,那么为每个字段分别写注解是很愚蠢的。在这种情况下,你有两个选择:
- 你可以将
JsonKeyNameConverter
的后代作为参数传递给
Object? toJson({
bool includeNullValues = false,
JsonKeyNameConverter? keyNameConverter,
}) {
...
}
方法
- 你可以这样注解整个类
@CamelToSnake()
class SimpleUserClassKeyNames {
String? firstName;
String? lastName;
int age = 0;
Gender? gender;
DateTime? dateOfBirth;
}
但请注意,字段注解优先于所有其他选项。它们会覆盖你在类级别或传递给 toJson()
方法参数的注解。参数会覆盖类级别注解。
因此,层次结构如下所示:
- 字段级别注解
- 参数(
toJson()
)- 类级别注解
- 参数(
还有一种可能的方式是全局设置此规则
通过设置 useCamelToStakeForAll
或 useSnakeToCamelForAll
标志为 true
(它们是互斥的)
在这种情况下,所有字段名称将自动使用其中之一进行转换。但如果手动应用了字段注解,则具有更高的优先级
例如:
useCamelToStakeForAll = true;
在这行之后,所有没有明确名称转换器的字段将被转换为蛇形命名
注意 另一种改变键名称的方法是向字段添加
@JsonKey(name: 'someNewName')
注解
在这种情况下,JsonKey
将优先于任何名称转换器
内置注解列表
字段规则
@JsonInclude()
- 字段级别注解,强制将键/值包含到结果JSON中,即使字段是私有的@JsonIgnore()
- 与JsonInclude
相反。它完全排除字段序列化@JsonKey()
- 字段规则的基类
验证器
@JsonValueValidator()
- 验证器的基类,可以扩展以验证任何类型的值@IntValidator()
- 此注解允许检查整数值是否在允许范围内。如果值超出范围,则会抛出异常@DoubleValidator()
- 与整数验证器相同,但适用于双精度@NumValidator()
- 与整数验证器相同,但适用于双精度@StringValidator()
- 可以验证字符串是否符合正则表达式模式@EmailValidator()
- 使用正则表达式验证电子邮件@PasswordValidator()
- 具有选项的密码验证器@CreditCardNumberValidator()
- 使用Luhn算法验证信用卡号码的验证器@PhoneValidator()
- 基于国家电话代码和电话掩码验证电话号码的验证器,比基于正则表达式的验证器更可靠@NameValidator()
- 验证用拉丁字母或西里尔字母书写的姓名。如果需要其他字母,应编写自己的验证器。以此为例
值转换器
@JsonValueConverter()
- 值转换器的基类,可以扩展以转换任何类型的值@JsonDateConverter()
- 允许你为DateTime
提供默认日期格式,例如yyyy-MM-dd
或其他@JsonIntConverter()
- 允许钳位int
值在最小值和最大值之间,或者如果实际值为null则赋予默认值@JsonNumConverter()
- 与int
转换器相同,但适用于num
@JsonKeyNameConverter()
- 值转换器的基类,可以用于编写自定义转换器@JsonTrimString()
- 修剪字符串的空白字符。左侧、右侧或两侧@JsonPhoneConverter()
- 根据参数格式化电话号码或删除格式
键转换器
@CamelToSnake()
- 将字段名称转换为snake_case_style
@SnakeToCamel()
- 将字段名称转换为camelCaseStyle
@FirstToUpper()
- 将字段名称的第一个字母转换为大写
toJson()
和 fromJson
方法也有 tryUseNativeSerializerMethodsIfAny
参数。
如果你传递 true
,它将尝试使用任何原生的 toJson
/ fromJson
方法。
这在你想使用使用 json_serializable
包生成的类时很有用。
这两个方法 toJson
和 fromJson
都有 onKeyConversion
参数。
它是一个回调函数,将在每个被转换的键上被调用。
这对于日志记录目的或其他目的(如创建数据库索引以理解转换后的键是什么)很有用
onKeyConversion: (ConvertedKey result) {
print(result);
}
无分类
@JsonIncludeParentFields()
- 允许toJson()
也添加父字段
编写自定义注解
上述的一些注解被标记为“基类”,你可以扩展它们并编写自定义逻辑。
我还会在这里添加一些类型
更多关于Flutter反射机制增强插件reflect_buddy的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
更多关于Flutter反射机制增强插件reflect_buddy的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html
当然,以下是如何在Flutter项目中使用reflect_buddy
插件来利用反射机制的示例代码。reflect_buddy
是一个Flutter插件,它允许你在运行时动态地访问和调用Dart对象的属性和方法。这对于需要动态行为或高度灵活性的应用非常有用。
首先,确保你已经在你的pubspec.yaml
文件中添加了reflect_buddy
依赖:
dependencies:
flutter:
sdk: flutter
reflect_buddy: ^最新版本号 # 请替换为实际可用的最新版本号
然后运行flutter pub get
来安装依赖。
接下来,我们来看一个基本的代码示例,展示如何使用reflect_buddy
来动态访问对象的属性和方法。
示例代码
- 创建一个示例类
首先,我们定义一个简单的类,包含一些属性和方法。
// example_class.dart
class ExampleClass {
String name;
int age;
ExampleClass(this.name, this.age);
void greet() {
print("Hello, my name is $name and I am $age years old.");
}
}
- 使用reflect_buddy
然后,我们编写一个Flutter小部件,使用reflect_buddy
来动态访问ExampleClass
的属性和方法。
// main.dart
import 'package:flutter/material.dart';
import 'package:reflect_buddy/reflect_buddy.dart';
import 'example_class.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: Text('Reflect Buddy Example'),
),
body: ReflectBuddyExample(),
),
);
}
}
class ReflectBuddyExample extends StatefulWidget {
@override
_ReflectBuddyExampleState createState() => _ReflectBuddyExampleState();
}
class _ReflectBuddyExampleState extends State<ReflectBuddyExample> {
late ExampleClass exampleInstance;
@override
void initState() {
super.initState();
exampleInstance = ExampleClass('Alice', 30);
}
void callDynamicMethod() {
// 使用reflect_buddy调用greet方法
ReflectBuddy.invokeMethod(exampleInstance, 'greet');
}
void accessDynamicProperty() {
// 使用reflect_buddy访问name属性
var name = ReflectBuddy.getProperty(exampleInstance, 'name');
print('Name: $name');
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
ElevatedButton(
onPressed: callDynamicMethod,
child: Text('Call greet method'),
),
ElevatedButton(
onPressed: accessDynamicProperty,
child: Text('Access name property'),
),
],
),
);
}
}
说明
- ExampleClass:这是一个简单的类,包含
name
和age
两个属性,以及一个greet
方法。 - ReflectBuddyExample:这是一个Flutter小部件,它初始化了一个
ExampleClass
的实例,并提供了两个按钮,一个用于动态调用greet
方法,另一个用于动态访问name
属性。 - ReflectBuddy.invokeMethod:这是
reflect_buddy
插件的方法,用于在运行时调用对象的方法。 - ReflectBuddy.getProperty:这是
reflect_buddy
插件的方法,用于在运行时访问对象的属性。
通过上述代码,你可以在Flutter应用中动态地访问和调用对象的属性和方法,利用reflect_buddy
插件提供的反射机制。请确保在实际使用中遵循最新的reflect_buddy
文档和API变更。