Flutter json 2 model with Built Value

Flutter json 2 model with Built Value

Flutter中json轉(zhuǎn)換model, 除了手動(dòng)轉(zhuǎn)之外, 就是利用第三方庫做一些代碼生成.
流行的庫有: json_serializablebuilt_value

本文介紹built_value的實(shí)際使用及問題處理.

Flutter中的json轉(zhuǎn)model方法

Flutter中json到model類型的轉(zhuǎn)換可以有多種方式:

  • 利用官方自帶的dart convert中的json解碼. 該方法只能將json轉(zhuǎn)換為List或Map, 剩下的工作需要手動(dòng)完成, 根據(jù)key取值賦值給model的字段.
  • 利用第三方的庫, 做代碼生成, 流行的庫有: json_serializablebuilt_value. 原理都是相同的, 先寫一些模板代碼, 說明一下model是什么樣子的, 然后運(yùn)行命令行生成一些代碼, 之后就可以很方便地調(diào)用, 將json轉(zhuǎn)換為model了.

使用json_serializable可以看:

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

built value使用指南

實(shí)例: 用github api拿到的events: https://api.github.com/events?per_page=10
如何轉(zhuǎn)化成model對(duì)象呢?

TDD

先寫個(gè)測試, 明確一下我們想要的目標(biāo).

test下建立一個(gè)文件, 比如叫json_test.dart.

里面寫main函數(shù)和兩個(gè)測試:

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");
  });
}

這里面應(yīng)該放json字符串的, 太長了我就省略了, 這樣看比較清晰.

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

這個(gè)Event類和方法我們都還沒有寫, 所以暫時(shí)報(bào)錯(cuò).

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

然后點(diǎn)Packages get.

Live Templates

這個(gè)是IntelliJ系IDE(包括Android Studio)的快捷設(shè)置, 目的是為了減少手動(dòng)輸入. (可選.)

打開Preferences, 搜Live Templates.
Dart的部分點(diǎn)+號(hào)新增一個(gè)Live Template.

下面Abbreviation選一個(gè)適當(dāng)?shù)目s寫, 比如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的關(guān)鍵字built, 就會(huì)出現(xiàn)要生成的代碼, 其中寫好自己的類名.

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

在其中輸入built按確認(rèn)之后, 輸入類名Event, 就建好了:

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

包括一個(gè)私有構(gòu)造和一個(gè)工廠方法. 此時(shí)會(huì)有一些紅色的報(bào)錯(cuò).
這里import 'package:built_value/built_value.dart'消除Built類的報(bào)錯(cuò).

根據(jù)觀察API: https://api.github.com/events 返回的json, 發(fā)現(xiàn)還應(yīng)該有Actor, Repo, Payload三個(gè)類.
也都按這個(gè)方法建立好.

然后在其中添加字段, 現(xiàn)在看起來是這樣了:

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是一個(gè)慣例, 表明這個(gè)文件是生成的代碼. part表示目前這個(gè)文件是另一個(gè)文件的一部分.

按照同樣的方法把幾個(gè)類都建好.

注意如果有列表字段, 要聲明為BuiltList類型.

運(yùn)行生成命令

生成命令:

flutter packages pub run build_runner build

需要持續(xù)構(gòu)建和可以用:

flutter packages pub run build_runner watch

這樣就不用每次改完代碼都需要跑一次命令了.

我們這里用watch, 因?yàn)檫€沒有改完.
運(yùn)行完成之后, 可以看到.g.dart的文件們都生成了, 報(bào)錯(cuò)也消失了.

寫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, 因?yàn)閎uilt value的json格式不是標(biāo)準(zhǔn)的, 而是所有字段逗號(hào)分隔的.
用了StandardJsonPlugin之后就轉(zhuǎn)換成了標(biāo)準(zhǔn)的JSON格式.

因?yàn)槲覀兣苊畹臅r(shí)候用的是watch, 所以保存修改后serializers.g.dart文件此時(shí)自動(dòng)生成了.

添加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';

此時(shí)其他幾個(gè)model類也要添加serializer, 比如Actor類中添加:

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

重新build生成代碼, 報(bào)錯(cuò)消失.

現(xiàn)在可以運(yùn)行測試:

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

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

來檢驗(yàn)單個(gè)的Event model建立.

可能會(huì)遇到的失敗情況:

  • 一些字段需要被標(biāo)記為可為空@nullable.
  • 一些字段名和key不匹配, 用@BuiltValueFieldwireName標(biāo)記.
    詳見后面的Troubleshooting部分.

如何反序列化頂層列表?

Event的API返回的是一個(gè)Event的數(shù)組: []. 這種怎么做呢?

這里有個(gè)issue就是關(guān)于這個(gè)問題, 里面的解決辦法挺好: 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數(shù)組的方法:

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

到這一步, 跑我們開頭寫的兩個(gè)測試應(yīng)該都綠了. 如果沒綠見Troubleshooting部分.

泛型的fromJson方法.

上面給serializers中添加了兩個(gè)方法. 其中第一個(gè)方法是一個(gè)泛型的fromJson方法.

我們測試中的:

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

也可以這樣寫:

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

這樣不用給每一個(gè)類都寫一個(gè)fromJson方法了.

Troubleshooting

可能會(huì)有的報(bào)錯(cuò), 問題原因和解決方式.

報(bào)錯(cuò)1: failed due to: Invalid argument(s): Unknown type on deserialization. Need either specifiedType or discriminator field.

比如這個(gè)樣子:

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.

這是因?yàn)?code>Event中依賴的類(Actor, Repo, Payload)沒有添加serializer.

比如Actor中:

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

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

報(bào)錯(cuò)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.

此時(shí), 先不要著急把字段標(biāo)記為@nullable.
而是要看這個(gè)字段是否真的為null, 很有可能是因?yàn)樽侄蚊Q和json中的key不匹配造成的, 比如json中是個(gè)蛇形命名.

查看了一下果然就是, 解決辦法:

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

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

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

報(bào)錯(cuò)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)

相關(guān)issue: https://github.com/dart-lang/convert/issues/10

解決的辦法就是在測試的字符串聲明前加一個(gè)r:

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

Model生成工具推薦

有個(gè)很棒的工具: https://charafau.github.io/json2builtvalue/
左邊輸入json字符串, 寫好命名, 點(diǎn)擊之后右邊就會(huì)出現(xiàn)那些本來需要手動(dòng)寫的代碼.

生成的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;
}

哈哈, 看到這里是不是有種被騙了的感覺.

有了這個(gè)很棒的工具之后根本不用自己很小心地寫一個(gè)一個(gè)model類了, 只需要寫一個(gè)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));

然后把要反序列化的類加進(jìn)來, 再跑命令行生成代碼, 就可以了.

經(jīng)歷一下前面的手動(dòng)過程可能理解得更好一些, 也知道各種問題的原因.
以后使用直接用工具就方便多了.

參考資料

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容