Flutter使用 json_serializable 解析 JSON 最佳方案

Posted 一叶飘舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter使用 json_serializable 解析 JSON 最佳方案相关的知识,希望对你有一定的参考价值。

Flutter 官方推荐的 json_serializable JSON 解析方案,之前感觉比较麻烦就没有使用,使用的是通过 app.quicktype.io 在线生成,具体可以查看之前写的一篇介绍文章: https://juejin.cn/post/6931265476928471053

app.quicktype.io 迟迟不支持空安全,我们又把解析方案换成了 FlutterJsonBeanFactory,这个插件会对 JSON 数据类型进行兼容处理,是个不错的亮点。

最近 FlutterJsonBeanFactory 也出现了一些小问题,生成的类中会导入不相关的包,这让我开始重新考虑官方方案。

毕竟官方方案一直有专人维护,功能也最全。

首页,我们先 介绍一下使用官方方案的步骤:

一、在 pubspec.yaml 文件中添加依赖

dependencies:
  json_annotation: ^4.5.0
 
dev_dependencies:
  json_serializable: ^6.2.0
  build_runner: ^2.1.11

二、手动创建模型类

import 'package:json_annotation/json_annotation.dart';
 
part 'user.g.dart';
 
@JsonSerializable()
class User 
  User(
    this.name,
    this.email,
    this.userId,
  );
 
  String? name;
  String? email;
  @JsonValue("user_id") 
  String? userId;
 
   
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
 
  Map<String, dynamic> toJson() => _$UserToJson(this);


字段少的话,还可以接受,如果有几十个字段,这块就很耗时间,这个也是我们之前没有选择官方方案的原因

三、执行构建命令

然后执行命令 flutter pub run build_runner build ,就可以生成解析 JSON 的代码。

最近几天,我发现一个插件:AutoJson

这个插件可以自动执行 flutter pub run build_runner build 命令,这给了我一个想法:那有没有一个插件,能够把上面三个步骤都给执行了?

就像 android 开发者使用的 GsonFormat 一样,输入类名,复制 JSON 数据到输入框。然后点击确定按钮,能够帮我们把上面三个步骤给自动执行了,那我们在使用官方方案时,不就省事省力了?

我搜索了一圈,居然没有发现这样的插件。

由于我就借鉴(chaoxi)一些插件,开发了一个可以自动生成 json_serializable Android Studio 插件,目前插件已经上传,在 Android Studio Plugin 搜索 FlutterJsonToDart 即可下载使用

插件功能截图:

插件介绍:

  • 输入 JSON 数据自动生成带 json_serializable 注解的 dart 模型类
  • 如果 pubspec.yaml 文件中没有添加 json_annotation、json_serializable、build_runner 依赖,会自动帮你添加
  • 自动执行命令 flutter pub run build_runner build --delete-conflicting-outputs

这个插件可以帮助大家简化使用 json_serializable,底层解析逻辑是 json_serializable 来处理的,这样即便以后不使用该插件,相关代码不受影响。

如果对插件生成的模型类,不满意,可以自己进行调整:比如一些状态字段改成枚举,或者有些模型类已经定义过,想进行复用。

修改完字段后,你可以通过快捷键 alt + j 或者通过菜单 Build -> Run flutter pub run build_runner watch 运行 watch 命令,在后台监控字段变化,如果字段发生变化,它会重新生成 xxx.g.dart。

如果大家对使用 json_serializable 有什么问题,也可以在评论中留言,后续有时间,我可以把使用 json_serializable 遇到的一些问题分享出来。

致谢:

  1. 输入框参考的是这个插件 FlutterJsonBeanFactory
  2. 执行 flutter 命令参考的是这个插件 AutoJson
  3. 判断 pubspec.yaml 中是否已经添加依赖参考这个插件:FlutterPubVersionChecker

Flutter json_serializable

1 添加项目依赖
dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.1.3
  json_serializable: ^3.2.0

2 创建实体类
  可以使用json_serializable库,将json 拷贝进去自动生成(简单json可以生成,在嵌套一层便会提示“不是一个正确的json”,不是特别好用)

 将生成得subject.dart文件拷贝到项目中,提示如下异常

3 生成.g.dart文件

cd 你的工程目录

flutter packages pub run build_runner build    //使用 build_runner 生成 .g.dart 文件

flutter packages pub run build_runner wacth    //监控生成文件,如果有改动时自动生成/更新 .g.dart 文件

还没有生成.g.dart文件或者报错的运行下面的命令

cd 你的工程目录

flutter packages pub run build_runner build --delete-conflicting-outputs  //删除并重新创建.g.dart文件

没有生成的再运行一下

flutter packages pub run build_runner build

 4、异常

F:\\demo\\flutter_demo\\LeanDemo>flutter packages pub run build_runner build
[INFO] Generating build script...
[INFO] Generating build script completed, took 282ms
 
[INFO] Creating build script snapshot......
[INFO] Creating build script snapshot... completed, took 17.8s
 
[INFO] Initializing inputs
[INFO] Building new asset graph...
[INFO] Building new asset graph completed, took 1.0s
 
[INFO] Checking for unexpected pre-existing outputs....
[INFO] Found 1 declared outputs which already exist on disk. This is likely because the`.dart_tool/build` folder was deleted, or you are submitting generated files to your source repository.
[SEVERE] Conflicting outputs were detected and the build is unable to prompt for permission to remove them. These outputs must be removed manually or the build can be run with `--delete-conflicting-outputs`. The outputs are: lib/a
/model/entity/subject.g.dart
pub finished with exit code 78

找到一个已声明的输出,该输出已存在于磁盘上。这可能是因为“.dart_tool/build”文件夹已删除,或者您正在将生成的文件提交到源存储库。

运行flutter packages pub run build_runner build不光生成了xxxx.g.dart。同时还成了一个dart_tool/build目录,只运行app是没有问题的,之前xxxx.g.dart还是可用的。但是再生成新的xxxx.g.dart因为缺少之前生成dart_tool/build目下的文件就会报错。

解决:

 
//清除之前生成的文件,如果直接运行下面的不行。可以先尝试运行这个
 flutter packages pub run build_runner clean
//可以直接运行这个
 flutter packages pub run build_runner build --delete-conflicting-outputs
 
//在重新生成.g.dart
flutter packages pub run build_runner build

Json自动反序列化——json_serializable(附源码)

前言

Google推出flutter这样一个新的高性能跨平台(Android,ios)快速开发框架之后,被业界许多开发者所关注。我在接触了flutter之后发现这个确实是一个好东西,好东西当然要和大家分享,对吧。

今天要跟大家分享的是Json反序列化的实现。相信做app的同学都会遇到这么一个问题,在向服务器请求数据后,服务器往往会返回一段json字符串。而我们要想更加灵活的使用数据的话需要把json字符串转化成对象。由于flutter只提供了json to Map。而手写反序列化在大型项目中极不稳定,很容易导致解析失败。所以今天给大家介绍的是flutter团队推荐使用的 json_serializable 自动反序列化。

你将学到什么

  • flutter中如何解析json对象
  • 如何使用自动生成工具生成代码
  • 如何测试你的数据

开始json反序列化

第一步:创建mock数据

在实际开发过程中,我们可能会对之前的一些代码进行修改。当我们代码功能复杂并且量足够大的时候,我们需要使用单元测试来保证新添加的代码不会影响之前所写的代码。而服务器的数据经常会变化,所以第一步当然是创建一个我们的mock数据啦。

这里使用了GITHUB/HackerNews的数据(GitHub - HackerNews/API: Documentation and Samples for the Official HN API)

abstract class JsonString
  static final String mockdata = ''' 
  "by" : "dhouston",
  "descendants" : 71,
  "id" : 8863,
  "kids" : [ 8952, 9224, 8917, 8884, 8887, 8943, 8869, 8958, 9005, 9671, 8940, 9067, 8908, 9055, 8865, 8881, 8872, 8873, 8955, 10403, 8903, 8928, 9125, 8998, 8901, 8902, 8907, 8894, 8878, 8870, 8980, 8934, 8876 ],
  "score" : 111,
  "time" : 1175714200,
  "title" : "My YC app: Dropbox - Throw away your USB drive",
  "type" : "story",
  "url" : "http://www.getdropbox.com/u/2/screencast.html"
''';

第二步:添加依赖

在pubspec.yaml中添加如下依赖

dependencies:
  # Your other regular dependencies here
  json_annotation: ^1.2.0

dev_dependencies:
  # Your other dev_dependencies here
  build_runner: ^0.10.2
  json_serializable: ^1.5.1

这里需要添加三个依赖,它们分别是:"json_annotation" "build_runner" 和 "json_serializable"。

请注意,yaml配置文件对于缩进要求十分严格,下面的build_runner和json_serializable应该是与flutter_test平级的,千万不要写在flutter_test缩进后,这样它会认为这两个是flutter_test的子集目录!

由于很多朋友在这一步遇到了问题,这里贴出源码

 

第三步:根据json创建实体类

我们这里根据上面的json数据写好了一个dart的实体类

class Data
  final String by;
  final int descendants;
  final int id;
  final List<int> kids;
  final int score;
  final int time;
  final String title;
  final String type;
  final String url;

  Data(this.by, this.descendants, this.id, this.kids, this.score, this.time,
    this.title, this.type, this.url);

我们在这里使用了dart语法糖创建了构造函数。具体请参考(Language tour | Dart)。

第四步:关联实体类文件

我们需要在我们的实体类中关联生成文件。

import 'package:json_annotation/json_annotation.dart';

part 'data.g.dart';

@JsonSerializable()
class Data 
  final String by;
  final int descendants;
  final int id;
  final List<int> kids;
  final int score;
  final int time;
  final String title;
  final String type;
  @JsonKey(nullable: false)
  final String url;

  Data(this.by, this.descendants, this.id, this.kids, this.score, this.time,
    this.title, this.type, this.url);

刚写完data.g.dart的会报错,这是正常的!因为我们还没生成解析文件

  • 为了使实体类文件找到生成文件,我们需要 part 'data.g.dart'。

第五步:生成Json解析文件

这里很重要!!!

我们要使用JsonSerializable生成代码的话必须要在需要生成代码的实体类前添加注解@JsonSerializable(),而要使用这个注解我们必须引入json_annotation/json_annotation.dart这个包。

import 'package:json_annotation/json_annotation.dart';

@JsonSerializable()
class Data
  final String by;
  final int descendants;
  final int id;
  final List<int> kids;
  final int score;
  final int time;
  final String title;
  final String type;
  final String url;

  Data(this.by, this.descendants, this.id, this.kids, this.score, this.time,
    this.title, this.type, this.url);

这里需要注意,flutter编码规范dart文件名统一小写,这样可以避免很多问题。ok这样实体类就创建完成啦。

那么问题来了,应该如何生成代码呢?

还记得我们刚才添加的build_runner的依赖吗,这时候我们就需要它来帮忙咯。

build_runner

build_runner是dart团队提供的一个生成dart代码文件的外部包。

我们在当前项目的目录下运行flutter packages pub run build_runner build

运行成功后我们应该能在这个实体类的下面发现一个新的文件 

 

这个data.g.dart就是build_runner根据JsonSerializable生成的json解析文件。
我们来看看这个生成的dart文件

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'data.dart';

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

Data _$DataFromJson(Map<String, dynamic> json) 
  return Data(
      by: json['by'] as String,
      descendants: json['descendants'] as int,
      id: json['id'] as int,
      kids: (json['kids'] as List)?.map((e) => e as int)?.toList(),
      score: json['score'] as int,
      time: json['time'] as int,
      title: json['title'] as String,
      type: json['type'] as String,
      url: json['url'] as String);


Map<String, dynamic> _$DataToJson(Data instance) => <String, dynamic>
      'by': instance.by,
      'descendants': instance.descendants,
      'id': instance.id,
      'kids': instance.kids,
      'score': instance.score,
      'time': instance.time,
      'title': instance.title,
      'type': instance.type,
      'url': instance.url
    ;

同学们请注意这段代码最上面的注释"// GENERATED CODE - DO NOT MODIFY BY HAND"。你可千万别手写生成文件啊哈哈哈哈。

这段代码生成实体类库的一个part,在老版本part中有一个抽象实体类的mixin,dart中使用基于mixin的继承来解决单继承的局限性。

新版本中声称了两个方法,fromJson和toJson方法,它们能干什么相信大家从名字上就能猜到了。

【对part感兴趣的同学可以参考Dart | 浅析dart中库的导入与拆分 - 掘金 Dart | 浅析dart中库的导入与拆分。

对mixin感兴趣的同学可以在(Language tour | Dart)了解更多关于mixin的知识。】

  • _$DataFromJson:它接收了一个map:Map<String, dynamic>,并将这个Map里的值映射为我们所需要的实体类对象。我们就可以使用这个方法,将存有json数据的map转化为我们需要的实体类对象。
  • _$DataToJson:将调用此方法的对象直接根据字段映射成Map。

而这两个都是私有方法,part让两个文件共享作用域与命名空间,所以我们需要将生成的方法暴露给外部。

然后我们再回到实体类中将 添加fromJson 和 toJson方法。

import 'package:json_annotation/json_annotation.dart';

@JsonSerializable()
class Data
  final String by;
  final int descendants;
  final int id;
  final List<int> kids;
  final int score;
  final int time;
  final String title;
  final String type;
  final String url;

  Data(this.by, this.descendants, this.id, this.kids, this.score, this.time,
    this.title, this.type, this.url);
//反序列化
  factory Data.fromJson(Map<String, dynamic> json) => _$DataFromJson(json);
//序列化
  Map<String, dynamic> toJson() => _$DataToJson(this);

  • 提供一个工厂构造方法Data.fromJson,该方法实际调用生成文件的DataFromJson方法。
  • 提供一个toJson()序列化对象的方法,实际调用生成文件的_$DataToJson()方法,并将调用对象解析生成Map<String ,dynamic>。

这样Json反序列化的工作就完成啦!

第六步:JSON反序列化

我们刚才实现了Map to Dart,可是我们需要的是json to dart。这时候就需要dart自带的 dart:convert 来帮助我们了。

dart:convert

dart:convert是dart提供用于在不同数据表示之间进行转换的编码器和解码器,能够解析JSON和UTF-8。

也就是说我们需要先将json数据使用dart:convert转成Map,我们就能通过Map转为dart对象了。

使用方法

Map<String ,dynamic> map = json.decode("jsondata");

知道了如何将jsonString解析成map以后我们就能直接将json转化为实体对象啦。

转化方法

    Data data = Data.fromJson(json.decode('jsondata'));

第七步:编写单元测试

flutter给我们提供了单元测试,它的好处在于,我们想要验证代码的正确性不用跑整个程序,每次只用跑一个单元测试文件。而且养成习惯编写单元测试以后,能够保证以后在开发过程中快速精确定位错误,避免新加入的代码破坏老的代码引起项目崩溃。每次应用启动前都会跑一遍单元测试以确保项目能够正确运行。在实际开发中我们应该使用的mock数据来作为测试数据来源。

使用方法
右键run这个测试文件

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:wmkids/data/mockdata.dart';
import 'package:wmkids/data/data.dart';

void main()
  group('jsonparse test', ()
    test('mockdata test', ()
      Data data1 = Data.fromJson(json.decode(JsonString.mockdata));
      expect(data1.url, 'http://www.getdropbox.com/u/2/screencast.html');
    );
  );

我们使用到了第一步创建的mock数据,并验证了该json的url,假如我们解析正确这个单元测试将会通过。

 这里的group是一组测试,一个group中可以有多个test验证我们的代码是否正确。

 

expect(data1,data2);会check我们的data1与data2的值是否相等,假如一样的话就会通过测试。假如不一样的话会告诉我们哪里不一样。

常用场景特殊处理办法

对象嵌套场景下的json解析

在json中经常会使用嵌套信息,我们在解析成dart文件的时候需要解析成对象嵌套。在这种场景下需要将编写步骤做一个调整。
我们需要在编写实体类的时候就带上工厂方法,因为对象存在依赖关系,先要保证子对象是serializable的才能保证父对象成功解析。

 这里提示有错误时正常的,然后再生成文件。

自定义字段

我们可以通过JsonKey自定义参数进行注释并自定义参数来自定义各个字段。例如:是否允许字段为空等。注意,这里不加任何JsonKey默认允许空json字段。

例如:

 这里的json使用了“-”作为字段名,而dart中只允许字母数字下划线作为变量名。所以我们必须对它进行特殊处理。@JsonKey(name="Nicehash-CNHeavy")来解析map。

然后再生成文件。 

 我们可以发现,生成文件已经将map中的NicehashCNHeavy替换成了Nicehash-CNHeavy。

泛型情况

注意,原着者写这篇文章的时候是在2018年,当时json_serializable还不支持泛型,

从v3.5.0 版本开始支持泛型,只需要在 @JsonSerializable() 注解中设置 genericArgumentFactories 为 true,同时需要对 fromJson 和 toJson 方法进行调整,即可支持泛型解析,具体可参考文章:Flutter 使用 json_serializable 解析 JSON 支持泛型 - 掘金

github源码参考

github.com/Vadaski/Vad…

以上是关于Flutter使用 json_serializable 解析 JSON 最佳方案的主要内容,如果未能解决你的问题,请参考以下文章

Flutter - 使用 google_sign_in 库时未找到 <Flutter/Flutter.h>

Flutter——如何使用 html 链接渲染 Flutter 文本 [重复]

flutter系列之:在flutter中使用导航Navigator

Flutter - 无法在flutter web中使用动态链接

无法使用 Flutter 1.22.3 编译 Flutter 应用程序

Flutter中使用flutter_html解析html文件