[译]Flutter Favorite 之超给力的辅助代码生成器 freezed - FromJson/ToJson

1,264 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第26天,点击查看活动详情


freezed | Dart Package (flutter-io.cn)

译时版本:2.2.0


FromJson/ToJson

虽然 Freezed 不会自己生成典型的 fromJson/toJson ,但它知道 json_serializable 是什么。

声明一个兼容 json_serializable 的类是很简单的。

考虑这样的代码片段:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'model.freezed.dart';

@freezed
class Model with _$Model {
  factory Model.first(String a) = First;
  factory Model.second(int b, bool c) = Second;
}

这个必需的修改是为了兼容 json_serializable 构成的下面两行:

  • 一个新的 part: part 'model.g.dart';
  • 目标类的新的构造方法:factory Model.fromJson(Map<String, dynamic> json) => _$ModelFromJson(json);

结果是:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'model.freezed.dart';
part 'model.g.dart';

@freezed
class Model with _$Model {
  factory Model.first(String a) = First;
  factory Model.second(int b, bool c) = Second;

  factory Model.fromJson(Map<String, dynamic> json) => _$ModelFromJson(json);
}

别忘了在 pubspec.yaml 中添加 json_serializable :

dev_dependencies:
  json_serializable:

大功告成!
加上这些改动,Freezed 会自动告诉 json_serializable 生成所有必需的 fromJson/toJson

注意
如果工厂方法使用的是 => , 那只会生成 fromJson 。

fromJSON - 带多个构造方法的类

对于有多个构造方法的类,Freezed 会检查为一个名为 runtimeType 的字符串元素对应的 JSON 响应,然后基于它的值选择对应的构造方法。 例如,有下面的构造方法:

@freezed
class MyResponse with _$MyResponse {
  const factory MyResponse(String a) = MyResponseData;
  const factory MyResponse.special(String a, int b) = MyResponseSpecial;
  const factory MyResponse.error(String message) = MyResponseError;

  factory MyResponse.fromJson(Map<String, dynamic> json) => _$MyResponseFromJson(json);
}

然后 Freezed 会使用每个 JSON 对象的 runtimeType 来选择构造方法,如下:

[
  {
    "runtimeType": "default",
    "a": "This JSON object will use constructor MyResponse()"
  },
  {
    "runtimeType": "special",
    "a": "This JSON object will use constructor MyResponse.special()",
    "b": 42
  },
  {
    "runtimeType": "error",
    "message": "This JSON object will use constructor MyResponse.error()"
  }
]

也可以用 @Freezed 和 @FreezedUnionValue 修饰符自定义不同的键值:

@Freezed(unionKey: 'type', unionValueCase: FreezedUnionCase.pascal)
class MyResponse with _$MyResponse {
  const factory MyResponse(String a) = MyResponseData;

  @FreezedUnionValue('SpecialCase')
  const factory MyResponse.special(String a, int b) = MyResponseSpecial;

  const factory MyResponse.error(String message) = MyResponseError;

  // ...
}

这会将前面的 json 更新为如下内容:

[
  {
    "type": "Default",
    "a": "This JSON object will use constructor MyResponse()"
  },
  {
    "type": "SpecialCase",
    "a": "This JSON object will use constructor MyResponse.special()",
    "b": 42
  },
  {
    "type": "Error",
    "message": "This JSON object will use constructor MyResponse.error()"
  }
]

如果想要为所有的类自定义键和值,可以在 build.yaml 中指定。例如:

targets:
  $default:
    builders:
      freezed:
        options:
          union_key: type
          union_value_case: pascal

如果你不想控制 JSON 响应,可以实现一个自定义的转换器。 这个自定义转换器需要实现决定使用哪个构造方法的逻辑。

class MyResponseConverter implements JsonConverter<MyResponse, Map<String, dynamic>> {
  const MyResponseConverter();

  @override
  MyResponse fromJson(Map<String, dynamic> json) {
    // type 的数据已经设置 (例如,因为我们自己进行了序列化)
    if (json['runtimeType'] != null) {
      return MyResponse.fromJson(json);
    }
    // 需要找到一些条件来知道它是哪种类型。例如,检查 json 中某些字段的存在。
    if (isTypeData) {
      return MyResponseData.fromJson(json);
    } else if (isTypeSpecial) {
      return MyResponseSpecial.fromJson(json);
    } else if (isTypeError) {
      return MyResponseError.fromJson(json);
    } else {
      throw Exception('Could not determine the constructor for mapping from JSON');
    }
 }

  @override
  Map<String, dynamic> toJson(MyResponse data) => data.toJson();
}

然后把自定义的转换器应用到构造方法参数的装饰符。

@freezed
class MyModel with _$MyModel {
  const factory MyModel(@MyResponseConverter() MyResponse myResponse) = MyModelData;

  factory MyModel.fromJson(Map<String, dynamic> json) => _$MyModelFromJson(json);
}

这样做的话 json serializable 可以使用 MyResponseConverter.fromJson() 和 MyResponseConverter.toJson() 来转换  MyResponse

也可以对包含 List 的构造方法参数使用自定义转换器。

@freezed
class MyModel with _$MyModel {
  const factory MyModel(@MyResponseConverter() List<MyResponse> myResponse) = MyModelData;

  factory MyModel.fromJson(Map<String, dynamic> json) => _$MyModelFromJson(json);
}

注意
为了序列化 Freezed 对象中嵌套的列表,应该要指定 @JsonSerializable(explicitToJson: true) 或着修改 build.yaml 文件中的 explicit_to_json (查看文档) 。

反序列化泛型类

为了反序列化/序列化泛型的 Freezed 对象,可以使 genericArgumentFactories 可用。
所有需要做的是修改  fromJson 方法的签名并在 freezed 配置中添加 genericArgumentFactories: true

@Freezed(genericArgumentFactories: true)
class ApiResponse<T> with _$ApiResponse<T> {
  const factory ApiResponse.data(T data) = ApiResponseData;
  const factory ApiResponse.error(String message) = ApiResponseError;

  factory ApiResponse.fromJson(Map<String, dynamic> json, T Function(Object?) fromJsonT) => _$ApiResponseFromJson(json, fromJsonT);
}

作为代替方案,可以如下修改 build.yaml 文件为整个工程开启 genericArgumentFactories

targets:
  $default:
    builders:
      freezed:
        options:
          generic_argument_factories: true

@JsonKey 注解如何使用?

所有传给构造方法参数的修饰符也会 "复制-粘贴" 到生成的属性。
因此,可以如下编写:

@freezed
class Example with _$Example {
  factory Example(@JsonKey(name: 'my_property') String myProperty) = _Example;

  factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
}

@JsonSerializable 注解如何使用?

可以在构造方法上面放置 @JsonSerializable 注解。 例如:

@freezed
class Example with _$Example {
  @JsonSerializable(explicitToJson: true)
  factory Example(@JsonKey(name: 'my_property') SomeOtherClass myProperty) = _Example;

  factory Example.fromJson(Map<String, dynamic> json) => _$ExampleFromJson(json);
}

如果想要为所有的类定义一些自定义的 json_serializable 标志(例如,explicit_to_json 或 any_map),可以参考 这里 的描述修改 build.yaml 文件。

也参考一下 修饰符 部分。