如何使用Dart / Flutter将枚举属性序列化/反序列化到Firestore中进行管理?

54

我需要将Flutter应用程序中的Dart对象存储到Firestore中。

这个对象包括一个枚举属性。

最佳解决方案是如何序列化/反序列化这个枚举属性?

  • 作为字符串

  • 作为整数

我找不到任何简单的方法来做到这一点。


请查看 Vnum https://github.com/AmirKamali/Flutter_Vnum - Amir.n3t
10个回答

86

Flutter能够生成JSON序列化代码。您可以在这里找到教程。它引用了json_annotation包,还支持枚举类型的序列化。所以,您只需要使用此工具,并使用@JsonValue注释您的枚举值。

根据代码文档

用于指定枚举值的序列化方式的注释。

基本上就是这样了。现在让我通过一个简单的示例代码来说明一下。假设有一个车辆枚举:

import 'package:json_annotation/json_annotation.dart';

enum Vehicle {
  @JsonValue("bike") BIKE,
  @JsonValue("motor-bike") MOTOR_BIKE,
  @JsonValue("car") CAR,
  @JsonValue("truck") TRUCK,
}

然后您可以在其中一个模型中使用此枚举,例如 vehicle_owner.dart,它看起来像这样:

import 'package:json_annotation/json_annotation.dart';

part 'vehicle_owner.g.dart';

@JsonSerializable()
class VehicleOwner{
  final String name;
  final Vehicle vehicle;

  VehicleOwner(this.name, this.vehicle);

  factory VehicleOwner.fromJson(Map<String, dynamic> json) =>
      _$VehicleOwnerFromJson(json);
  Map<String, dynamic> toJson() => _$VehicleOwnerToJson(this);
}

根据json生成指南,您需要提供以下内容。现在,您需要运行构建器或watcher,让Flutter生成代码:

flutter pub run build_runner build

那么生成的代码将如下所示。看一下已经根据您的@JsonValue注解生成的_$VehicleEnumMap

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'vehicle_owner.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

// more generated code omitted here ....

const _$VehicleEnumMap = {
  Vehicle.BIKE: 'bike',
  Vehicle.MOTOR_BIKE: 'motor-bike',
  Vehicle.CAR: 'car',
  Vehicle.TRUCK: 'truck',
};

@tbm98 很高兴能帮助您。 - Adam

34

最新的枚举实现

简而言之,使用以下最新的枚举序列化实现:

fromJson -> YourEnum.values.byName("property")

toJson -> YourEnum.property.name

在您的枚举上使用 toJson/fromJson

只需将这两个函数添加到您的枚举中即可。 请注意,您也可以简单地在类中创建这些函数。

enum Manufacturer {
  mercedes,
  volkswagen,
  toyota,
  ford;

  String toJson() => name;
  static Manufacturer fromJson(String json) => values.byName(json);
}

例子

class Car {
  final String name;
  final Manufacturer manufacturer;

  Car(this.name, this.manufacturer);

  Map<String, dynamic> toJson() {
    return {
      "name": name,
      "manufacturer": manufacturer.toJson(), // Alternative: manufacturer.name
    };
  }

  static Car fromJson(Map<String, dynamic> jsonData) => Car(
        jsonData['name'],
        Manufacturer.fromJson(jsonData['manufacturer']), // Alternative: Manufacturer.values.byName(jsonData['manufacturer'])
      );
}

我喜欢你使用最新的枚举实现方式(.name和.value.byName)。手动编写JSON生成器对于像你这样简单的事情来说是可以的,但是对于包含枚举的大型对象,我不想这样做。我更愿意使用json_serializable。因此,在枚举上使用注释似乎是有意义的。也许我漏掉了什么。 - Bill Turner
1
非常好!现在全部在Dart API中,再也不需要为这样一件容易的事情添加包了 :) - jksevend
有没有办法将toJson()和fromJson()逻辑提取到 mixin 中,以便它们可以轻松地添加到任何枚举中?这对于函数完全相同的情况非常有用。 - Kshitiz Kamal
1
尽管您可以在Enum上创建EnumHelper扩展,但据我所知,在这种通用类中无法访问YouEnum.values. 您可以通过以下方式实现toJson(). 您可能会对此问题感兴趣,他们正在讨论将此类函数实现到原生Dart语言中。 - Paul
@Paul 抱歉,我实际上没有运行代码,我只是在其他地方看到了这个问题的讨论。对于过时的信息感到抱歉! - Luke Hutchison
Dart的序列化一直是一段“有趣”的旅程。希望有一天它会稳定下来,所有误导性的旧文档都会消失。 - Chris Nadovich

14

Gunter的回答是正确的,只是有点不完整。

JSON可序列化确实可以处理将枚举转换为字符串并从字符串转换回来,以下是生成的示例代码:

const _$HoursEnumMap = <Hours, dynamic>{
  Hours.FullTime: 'FullTime',
  Hours.PartTime: 'PartTime',
  Hours.Casual: 'Casual',
  Hours.Contract: 'Contract',
  Hours.Other: 'Other'
};

然后,它会使用这个相当晦涩的函数将其转换回来:

T _$enumDecode<T>(Map<T, dynamic> enumValues, dynamic source) {
  if (source == null) {
    throw ArgumentError('A value must be provided. Supported values: '
        '${enumValues.values.join(', ')}');
  }
  return enumValues.entries
      .singleWhere((e) => e.value == source,
          orElse: () => throw ArgumentError(
              '`$source` is not one of the supported values: '
              '${enumValues.values.join(', ')}'))
      .key;
}

我对此感到非常厌烦,因此决定制作一个小型软件包来简化复杂性,并且对我来说非常方便:

https://pub.dev/packages/enum_to_string

至少它经过了单元测试,而不是复制/粘贴的解决方案。欢迎添加或提出请求。


1
如果您按照此处描述的方式使用json生成器https://flutter.dev/docs/development/data-and-backend/json#code-generation,那么当它遇到带有@JsonValue的枚举值时,_$enumDecode就是自动生成的方法。如果您可以将json_serializable和enum_to_string结合使用,以实现与Java中objectMapper.setPropertyNamingStrategy()相同的用例,那将非常酷。 - Adam
嘿@Adam。我现在正在尝试扩展json可序列化,以生成一个“populate”函数,所以我已经深入其中了。这不是一个完美的解决方案,但你可以使用@JsonKey(toJson:(value)=>...)来注释一个字段,并使用fromJson类似的方式。这将允许您手动处理转换并在需要时具有不同的值。 - Ryan Knell
非常感谢您修补了Dart中的这个讨厌的漏洞。令人遗憾的是,得票最高的答案是代码生成。 - shawnblais
我的使用案例是 enum Status { void }void 是 Dart 中的保留字,但我们在其他语言系统中有这个值。在 Dart 中,我们将枚举声明为 enum Status { void_ },有什么建议如何处理? - LHJ

11

我的做法是只保存枚举的索引。

假设你有一个枚举:

enum Location {
  EARTH,
  MOON,
  MARS,
}

并且有一个包含该枚举的类,其中具有以下方法:

/// Returns a JSON like Map of this User object
  Map<String, dynamic> toJSON() {
    return {
      "name": this.name,
      "location": this.location.index,
    };
  }

  /// Returns [Player] build from a map with informationen
  factory Player.fromJson(Map<String, dynamic> parsedJson) {
    return new Player(
      name: parsedJson['name'],
      location: Location.values.elementAt(
        parsedJson['location'],
      ),
    );
  }

更新

在@JamesAllen提到可维护性的回答后,我想出了这个新的解决方案:

extension LocationExtension on Location {
  String get name => describeEnum(this);
}

Location parseLocation(final String locationName) {
  switch (locationName) {
    case 'earth':
      return Location.earth;
    case 'moon':
      return Location.moon;
    case 'mars':
      return Location.mars;
    default:
      throw Exception('$locationName is not a valid Location');
  }
}

在你的toJson/fromJson中进行以下操作:

/// Returns a JSON like Map of this User object
  Map<String, dynamic> toJSON() {
    return {
      "name": this.name,
      "location": this.location.name,
    };
  }

  /// Returns [Player] build from a map with informationen
  factory Player.fromJson(Map<String, dynamic> parsedJson) {
    return new Player(
      name: parsedJson['name'],
      location: parseLocation(parsedJson['location']),
    );

我非常喜欢这种方法,特别是随着Cloud Firestore 2.0.0带来的新的withConverter! - Nicolas Durant
8
这种方法的缺点是可维护性不高:下一个修改枚举类型的人(甚至可能是在一年后的你自己)可能不知道索引和顺序的关键性。例如,如果在列表开头添加了一个值,那么就会出现问题。 - James Allen
@JamesAllen 存在单元测试是有原因的。 - Pedro Paulo Amorim
@PedroPauloAmorim 单元测试不能弥补编写脆弱代码的缺陷。 - Justin

4
如果有人使用“Dart Data Class Generator”,则需要用// enum comment进行注释。

enter image description here

enter image description here


1
与jksevend的方法类似(保存枚举索引),我是这样解决的,它保存了一个可读的字符串。 好处:您可以在现有条目之间插入新的枚举条目而不会破坏加载/保存!
class Player
{
    String name;
    Gender gender;

    // functions for jsonEncode and jsonDecode!
    Player.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        gender = getGenderEnum(json['gender']);

    Map<String, dynamic> toJson() => {
      'name': name,
      'gender': getGenderText(gender);
    };
}
enum Gender
{
    MALE,
    FEMALE,
    DIVERSE,
}
String getGenderText(Gender gen)
{
    switch(gen)
        case Gender.MALE:
            return "male";
        case Gender.FEMALE:
            return "female";
        case Gender.DIVERSE:
            return "diverse";
}
Gender getGenderEnum(String gen) {
    for (Gender candidate in Gender.values) {
        if (gen == getGenderText(candidate))
            return candidate;
    }
    return Gender.MALE;
}

1
自从Dart 2.17发布以来,您现在可以扩展Enum类。
诀窍是分配一个固定/不可变的值(int、string或其他您喜欢的类型),用作JSON表示。
enum Color {
  /// The [jsonValue] must not change in time!
  red(10), // Can be numbers
  blue(20),
  green("myGreen"), // Can be strings as well
  gray(40),
  yellow(50);

  final dynamic jsonValue;
  const Color(this.jsonValue);
  static Color fromValue(jsonValue) =>
      Color.values.singleWhere((i) => jsonValue == i.jsonValue);
}

main() {
  var myValue = Color.green.jsonValue;
  var myEnum = Color.fromValue(myValue);
  print(myEnum);
}

这个概念以及更多内容已经在我的新 jsonize 1.4.0包中实现,它可以轻松序列化枚举、日期时间和您自己的任何类。以下是一个简单的枚举示例:
import 'package:jsonize/jsonize.dart';

enum Color with JsonizableEnum {
  red("rd"),
  blue("bl"),
  green("grn"),
  gray("gry"),
  yellow("yl");

  @override
  final dynamic jsonValue;
  const Color(this.jsonValue);
}

void main() {
  // Register your enum
  Jsonize.registerEnum(Color.values);

  Map<String, dynamic> myMap = {
    "my_num": 1,
    "my_str": "Hello!",
    "my_color": Color.green,
  };
  var jsonRep = Jsonize.toJson(myMap);
  var hereIsMyMap = Jsonize.fromJson(jsonRep);
  print(hereIsMyMap);
}

这是关于 jsonize 能力的扩展示例:
import 'package:jsonize/jsonize.dart';

enum Color with JsonizableEnum {
  red("rd"),
  blue("bl"),
  green("grn"),
  gray("gry"),
  yellow("yl");

  @override
  final dynamic jsonValue;
  const Color(this.jsonValue);
}

class MyClass implements Jsonizable<MyClass> {
  String? str;
  MyClass([this.str]);
  factory MyClass.empty() => MyClass();

  // Jsonizable implementation
  @override
  String get jsonClassCode => "mc";
  @override
  dynamic toJson() => str;
  @override
  MyClass? fromJson(value) => MyClass(value);
}

void main() {
  // Register enums and classes
  Jsonize.registerEnum(Color.values);
  Jsonize.registerClass(MyClass.empty());

  Map<String, dynamic> myMap = {
    "my_num": 1,
    "my_str": "Hello!",
    "my_color": Color.green,
    "my_dt": DateTime.now(),
    "my_class": MyClass("here I am!")
  };
  var jsonRep = Jsonize.toJson(myMap);
  var hereIsMyMap = Jsonize.fromJson(jsonRep);
  print(hereIsMyMap);
}

1

2
最好的方法是使用枚举整数值。"你需要注意只在末尾添加新的枚举值[...]否则持久化的值将变为无效"对我来说是矛盾的。 - Matthias S
我也觉得Dart中的枚举有些受限。但是,借助新的扩展方法,手动创建从/到转换器变得更加方便了。 - Günter Zöchbauer
@Philippe Fanaro,感谢您的编辑建议,但我认为最好您将其添加为评论。 - Günter Zöchbauer
@GünterZöchbauer,根据Joel Spolsky的说法(https://youtu.be/yBDWgWBEbVQ),那种被认为增加价值的编辑应该放在答案中而不是评论中(如果我的理解有误,请指出!)。评论区应该只是针对答案改进的讨论;他甚至打算在未来一周后删除评论。我知道这只是一个观点(他有具体的原因),但我非常赞同。无论如何,如果您仍然不同意,我会将我的评论发布在答案中。 - Philippe Fanaro

0

在启动我的第一个AWS Amplify项目时,我在他们的库中发现了这个有趣的方法:

// only to be used internally by amplify-flutter library
T? enumFromString<T>(String? key, List<T> values) =>
    values.firstWhereOrNull((v) => key == enumToString(v));

它的调用方式如下:

Post.fromJson(Map<String, dynamic> json)
      : id = json['id'],
        type = enumFromString<PostCategory>(json['type'], PostCategory.values),
        ... // Other props

我必须承认,这种方法比我使用开关读取字符串和输出枚举的自定义方法要聪明得多。现在我知道我不会很快被大型科技公司雇用了...

// JSON deserialization
PostStatusE getPostStatusEnumByString(String type) {
  switch (type) {
    case "draft":
      return PostStatusE.draft;
      break;
      ...

0

我最喜欢的方法是使用built_value包,它具有EnumClass,允许使用注释控制序列化值。这似乎是最安全和最强大的解决方案,因为您完全将序列化值与枚举名称或其索引分离,使您能够重命名和重新排序枚举值而不会破坏序列化。对我来说,这比其他答案都更胜一筹。

缺点是需要进行一些设置。将以下内容添加到pubspec.yaml中(用最新版本替换版本):

dependencies:
   built_collection: ^5.1.1
   built_value: ^8.1.2

...

dev_dependencies:
   build_runner: ^2.1.4
   built_value_generator: ^8.1.2

然后像这样编写你的枚举 - 使用wireNumber注解来告诉它你想要每个枚举序列化为的整数值。或者,如果你想要序列化为字符串,请将其替换为wireName,例如@BuiltValueEnumConst(wireName: 'foo')

part 'my_enum.g.dart';

class MyEnum extends EnumClass {

  // Use wireNumber to serialise to an int, or wireName to serialise to a String
  @BuiltValueEnumConst(wireNumber: 0)
  static const MyEnum foo = _$foo;
  @BuiltValueEnumConst(wireNumber: 1)
  static const MyEnum bar = _$bar;

  const MyEnum._(String name) : super(name);

  static BuiltSet<MyEnum> get values => _$values;
  static MyEnum valueOf(String name) => _$valueOf(name);
  static Serializer<MyEnum> get serializer => _$myEnumSerializer;
}

你还需要一个包含以下内容的文件(可以将其命名为enum_serializers.dart):
library serializers;

import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';

part 'enum_serializers.g.dart';

// add all of the built value types that require serialization
@SerializersFor([
    MyEnum,
    // add any more enums you need serializing here
])


// Also add StandardJsonPlugin. Without this, it will by default output value lists instead of a JSON-compatible value map
final Serializers enumSerialisers = (_$enumSerializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

然后生成相应的.g.dart文件:

flutter packages pub run build_runner build --delete-conflicting-outputs

现在你可以像这样进行序列化和反序列化:

final enumValue = MyEnum.foo;

// serialize:
int serializedValue = enumSerialisers.serializeWith(MyEnum.serializer, enumValue) as int;
// deserialize:
EnumValue deserializedValue = enumSerialisers.deserializeWith(MyEnum.serializer, serializedValue) as EnumValue;


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接