Flutter json 2 model with Built Value

Flutter json 2 model with Built Value

Flutter中json转换model, 除了手动转之外, 就是利用第三方库做一些代码生成.
流行的库有: json_serializablebuilt_value

本文介绍built_value的实际使用及问题处理.

Flutter中的json转model方法

Flutter中json到model类型的转换可以有多种方式:

  • 利用官方自带的dart convert中的json解码. 该方法只能将json转换为List或Map, 剩下的工作需要手动完成, 根据key取值赋值给model的字段.
  • 利用第三方的库, 做代码生成, 流行的库有: json_serializablebuilt_value. 原理都是相同的, 先写一些模板代码, 说明一下model是什么样子的, 然后运行命令行生成一些代码, 之后就可以很方便地调用, 将json转换为model了.

使用json_serializable可以看:

本篇文章主要介绍built value的使用.

built value使用指南

实例: 用github api拿到的events: https://api.github.com/events?per_page=10
如何转化成model对象呢?

TDD

先写个测试, 明确一下我们想要的目标.

test下建立一个文件, 比如叫json_test.dart.

里面写main函数和两个测试:

void main() {
  test("parse events list", () {
    const jsonString = """replace with events list json string""";

    expect(Event.fromEventsListJson(jsonString).first.id, "11732023561");
  });

  test("parse event", () {
    const jsonString = """replace with event json string""";

    expect(Event.fromJson(jsonString).id, "11732036753");
  });
}

这里面应该放json字符串的, 太长了我就省略了, 这样看比较清晰.

"""之后可以支持多行. (IDE里面可以折叠的.)

这个Event类和方法我们都还没有写, 所以暂时报错.

setup

添加依赖, 去package页面看添加什么版本: https://pub.dev/packages/built_value

pubspec.yaml中添加:

dependencies:
  flutter:
    sdk: flutter

  # other dependencies here

  built_value: ^7.0.9
  built_collection: ^4.3.2

dev_dependencies:
  flutter_test:
    sdk: flutter

  # other dev_dependencies here

  build_runner: ^1.8.0
  built_value_generator: ^7.0.9

然后点Packages get.

Live Templates

这个是IntelliJ系IDE(包括Android Studio)的快捷设置, 目的是为了减少手动输入. (可选.)

打开Preferences, 搜Live Templates.
Dart的部分点+号新增一个Live Template.

下面Abbreviation选一个适当的缩写, 比如built.
Template text贴入这段:

abstract class $CLASS_NAME$ implements Built<$CLASS_NAME$, $CLASS_NAME$Builder> {
  $CLASS_NAME$._();
  factory $CLASS_NAME$([void Function($CLASS_NAME$Builder) updates]) = _$$$CLASS_NAME$;
}

Applicable in Dart选: top-level.

建好之后以后就直接用啦.

建立models抽象类

输入刚才建立的live template的关键字built, 就会出现要生成的代码, 其中写好自己的类名.

比如我们要建立的model类型是Event类.
新建event.dart文件.

在其中输入built按确认之后, 输入类名Event, 就建好了:

abstract class Event implements Built<Event, EventBuilder> {
  Event._();
  factory Event([void Function(EventBuilder) updates]) = _$Event;
}

包括一个私有构造和一个工厂方法. 此时会有一些红色的报错.
这里import 'package:built_value/built_value.dart'消除Built类的报错.

根据观察API: https://api.github.com/events 返回的json, 发现还应该有Actor, Repo, Payload三个类.
也都按这个方法建立好.

然后在其中添加字段, 现在看起来是这样了:

import 'package:built_value/built_value.dart';
// imports for models

part 'event.g.dart';

abstract class Event implements Built<Event, EventBuilder> {
  String get id;

  String get type;

  Actor get actor;

  Repo get repo;

  Payload get payload;

  bool get public;

  String get createdAt;

  Event._();

  factory Event([void Function(EventBuilder) updates]) = _$Event;
}

很重要的一步, 就是在类前面添加上一句: part 'event.g.dart';.
g.dart是一个惯例, 表明这个文件是生成的代码. part表示目前这个文件是另一个文件的一部分.

按照同样的方法把几个类都建好.

注意如果有列表字段, 要声明为BuiltList类型.

运行生成命令

生成命令:

flutter packages pub run build_runner build

需要持续构建和可以用:

flutter packages pub run build_runner watch

这样就不用每次改完代码都需要跑一次命令了.

我们这里用watch, 因为还没有改完.
运行完成之后, 可以看到.g.dart的文件们都生成了, 报错也消失了.

写Serializers

新建文件serializers.dart.

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

// imports for models

part 'serializers.g.dart';

@SerializersFor(const [
  Event,
  Actor,
  Repo,
  Payload,
])
final Serializers serializers =
    (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

@SerializersFor里面列出想要序列化的类.

注意这里要加上StandardJsonPlugin, 因为built value的json格式不是标准的, 而是所有字段逗号分隔的.
用了StandardJsonPlugin之后就转换成了标准的JSON格式.

因为我们跑命令的时候用的是watch, 所以保存修改后serializers.g.dart文件此时自动生成了.

添加model序列化和反序列化代码

在Event类中添加:

  static Serializer<Event> get serializer => _$eventSerializer;

  String toJson() {
    return json.encode(serializers.serializeWith(Event.serializer, this));
  }

  static Event fromJson(String jsonString) {
    return serializers.deserializeWith(
        Event.serializer, json.decode(jsonString));
  }

import中除了model类还有:

import 'dart:convert';

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

此时其他几个model类也要添加serializer, 比如Actor类中添加:

static Serializer<Actor> get serializer => _$actorSerializer;

重新build生成代码, 报错消失.

现在可以运行测试:

  test("parse event", () {
    const jsonString = """replace with event json string""";

    expect(Event.fromJson(jsonString).id, "11732036753");
  });

来检验单个的Event model建立.

可能会遇到的失败情况:

  • 一些字段需要被标记为可为空@nullable.
  • 一些字段名和key不匹配, 用@BuiltValueFieldwireName标记.
    详见后面的Troubleshooting部分.

如何反序列化顶层列表?

Event的API返回的是一个Event的数组: []. 这种怎么做呢?

这里有个issue就是关于这个问题, 里面的解决办法挺好: https://github.com/google/built_value.dart/issues/565

serializers.dart中添加方法:

T deserialize<T>(dynamic value) =>
    serializers.deserializeWith<T>(serializers.serializerForType(T), value);

BuiltList<T> deserializeListOf<T>(dynamic value) => BuiltList.from(
    value.map((value) => deserialize<T>(value)).toList(growable: false));

其中BuiltList需要import 'package:built_collection/built_collection.dart';.

反序列化Event数组的方法:

  static List<Event> fromEventsListJson(String jsonString) {
    final BuiltList<Event> listOfEvents =
        deserializeListOf<Event>(json.decode(jsonString));
    return listOfEvents.toList();
  }

到这一步, 跑我们开头写的两个测试应该都绿了. 如果没绿见Troubleshooting部分.

泛型的fromJson方法.

上面给serializers中添加了两个方法. 其中第一个方法是一个泛型的fromJson方法.

我们测试中的:

expect(Event.fromJson(jsonString).id, "11732036753");

也可以这样写:

expect(deserialize<Event>(json.decode(jsonString)).id, "11732036753");

这样不用给每一个类都写一个fromJson方法了.

Troubleshooting

可能会有的报错, 问题原因和解决方式.

报错1: failed due to: Invalid argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field.

比如这个样子:

Deserializing '[id, 11732036753, type, PushEvent, actor, {id: 54496419, login: supershell201...' to 'Event' failed due to: Invalid argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field.

这是因为Event中依赖的类(Actor, Repo, Payload)没有添加serializer.

比如Actor中:

static Serializer<Actor> get serializer => _$actorSerializer;

添加上重新build生成代码即可.

报错2: Tried to construct class "XXX" with null field

比如:

Deserializing '[id, 11732036753, type, PushEvent, actor, {id: 54496419, login: supershell201...' to 'Event' failed due to: Deserializing '[id, 54496419, login, supershell2019, display_login, supershell2019, gravatar...' to 'Actor' failed due to: Tried to construct class "Actor" with null field "displayLogin". This is forbidden; to allow it, mark "displayLogin" with @nullable.

此时, 先不要着急把字段标记为@nullable.
而是要看这个字段是否真的为null, 很有可能是因为字段名称和json中的key不匹配造成的, 比如json中是个蛇形命名.

查看了一下果然就是, 解决办法:

  @BuiltValueField(wireName: 'display_login')
  String get displayLogin;

如果字段真的是有可能为null的情况, 那么加上@nullable:
比如:

  @BuiltValueField(wireName: 'ref_type')
  @nullable
  String get refType;

报错3: FormatException: Control character in string

比如:

FormatException: Control character in string (at line 25, character 129)
... replica::on_client_write(dsn::message_ex *request, bool ignore_throttling)

相关issue: https://github.com/dart-lang/convert/issues/10

解决的办法就是在测试的字符串声明前加一个r:

const jsonString = r"""replace with events list json string""";

Model生成工具推荐

有个很棒的工具: https://charafau.github.io/json2builtvalue/
左边输入json字符串, 写好命名, 点击之后右边就会出现那些本来需要手动写的代码.

生成的Event类是这样:

library event;

import 'dart:convert';

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

part 'event.g.dart';

abstract class Event implements Built<Event, EventBuilder> {
  Event._();

  factory Event([updates(EventBuilder b)]) = _$Event;

  @BuiltValueField(wireName: 'id')
  String get id;
  @BuiltValueField(wireName: 'type')
  String get type;
  @BuiltValueField(wireName: 'actor')
  Actor get actor;
  @BuiltValueField(wireName: 'repo')
  Repo get repo;
  @BuiltValueField(wireName: 'payload')
  Payload get payload;
  @BuiltValueField(wireName: 'public')
  bool get public;
  @BuiltValueField(wireName: 'created_at')
  String get createdAt;
  String toJson() {
    return json.encode(serializers.serializeWith(Event.serializer, this));
  }

  static Event fromJson(String jsonString) {
    return serializers.deserializeWith(
        Event.serializer, json.decode(jsonString));
  }

  static Serializer<Event> get serializer => _$eventSerializer;
}

哈哈, 看到这里是不是有种被骗了的感觉.

有了这个很棒的工具之后根本不用自己很小心地写一个一个model类了, 只需要写一个serializers.dart文件:

part 'serializers.g.dart';

@SerializersFor(const [
  Event,
  Actor,
  Repo,
  Payload,
])
final Serializers serializers =
    (_$serializers.toBuilder()..addPlugin(StandardJsonPlugin())).build();

T deserialize<T>(dynamic value) =>
    serializers.deserializeWith<T>(serializers.serializerForType(T), value);

BuiltList<T> deserializeListOf<T>(dynamic value) => BuiltList.from(
    value.map((value) => deserialize<T>(value)).toList(growable: false));

然后把要反序列化的类加进来, 再跑命令行生成代码, 就可以了.

经历一下前面的手动过程可能理解得更好一些, 也知道各种问题的原因.
以后使用直接用工具就方便多了.

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容