Flutter反射机制增强插件reflect_buddy的使用

发布于 1周前 作者 sinazl 来自 Flutter

Flutter反射机制增强插件reflect_buddy的使用

pub.devstyle: effective dart

基于反射(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 可以处理基本结构:MapList。它不能处理更复杂的类型,如 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 实例。它们在内部使用相同的逻辑。所以只需选择你喜欢的方法。

  1. 直接在类型上调用 fromJson 方法(这里需要括号来区分这个调用和静态方法调用)
final containerInstance = (ContainerWithCustomUsers).fromJson(containerWithUsers);
  1. 使用泛型简写方法
final containerInstance = fromJson<ContainerWithCustomUsers>(containerWithUsers);
  1. 对象扩展方法
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() 方法只序列化一个级别的实例。这意味着它忽略了任何超类字段。但在某些情况下,你也想包含父类字段。例如,idcreatedAtupdatedAt 是所有模型的常见字段。没有必要在每个模型中写入它们。只需在超类中声明它们,并在子类中添加 @JsonIncludeParentFields() 注解。

class BaseModel {
  int? id;
  DateTime? createdAt;
  DateTime? updatedAt;
}

@JsonIncludeParentFields()
class User extends BaseModel {
  int? age;
  String? firstName;
  String? lastName;
}

还可以通过设置全局 alwaysIncludeParentFieldstrue 来全局设置,但要小心使用此功能,因为它可能会在不需要时暴露一些敏感信息。最好在每个需要它的类上使用 @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}

当然,如果你对所有字段使用相同的键命名策略,那么为每个字段分别写注解是很愚蠢的。在这种情况下,你有两个选择:

  1. 你可以将 JsonKeyNameConverter 的后代作为参数传递给
  Object? toJson({
    bool includeNullValues = false,
    JsonKeyNameConverter? keyNameConverter,
  }) {
    ...  
  }

方法

  1. 你可以这样注解整个类
@CamelToSnake()
class SimpleUserClassKeyNames {

  String? firstName;
  String? lastName;
  int age = 0;
  Gender? gender;
  DateTime? dateOfBirth;
}

但请注意,字段注解优先于所有其他选项。它们会覆盖你在类级别或传递给 toJson() 方法参数的注解。参数会覆盖类级别注解。

因此,层次结构如下所示:

  • 字段级别注解
    • 参数(toJson()
      • 类级别注解

还有一种可能的方式是全局设置此规则 通过设置 useCamelToStakeForAlluseSnakeToCamelForAll 标志为 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 包生成的类时很有用。

这两个方法 toJsonfromJson 都有 onKeyConversion 参数。 它是一个回调函数,将在每个被转换的键上被调用。 这对于日志记录目的或其他目的(如创建数据库索引以理解转换后的键是什么)很有用

onKeyConversion: (ConvertedKey result) {
  print(result);
}

无分类

  • @JsonIncludeParentFields() - 允许 toJson() 也添加父字段

编写自定义注解

上述的一些注解被标记为“基类”,你可以扩展它们并编写自定义逻辑。

我还会在这里添加一些类型


更多关于Flutter反射机制增强插件reflect_buddy的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于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来动态访问对象的属性和方法。

示例代码

  1. 创建一个示例类

首先,我们定义一个简单的类,包含一些属性和方法。

// 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.");
  }
}
  1. 使用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:这是一个简单的类,包含nameage两个属性,以及一个greet方法。
  • ReflectBuddyExample:这是一个Flutter小部件,它初始化了一个ExampleClass的实例,并提供了两个按钮,一个用于动态调用greet方法,另一个用于动态访问name属性。
  • ReflectBuddy.invokeMethod:这是reflect_buddy插件的方法,用于在运行时调用对象的方法。
  • ReflectBuddy.getProperty:这是reflect_buddy插件的方法,用于在运行时访问对象的属性。

通过上述代码,你可以在Flutter应用中动态地访问和调用对象的属性和方法,利用reflect_buddy插件提供的反射机制。请确保在实际使用中遵循最新的reflect_buddy文档和API变更。

回到顶部