Json解析是平时日常开发的一个非常重要的部分,大部分从接口返回的数据都是Json格式,客户端通过解析Json数据来进行UI界面的绘制和展示。

Flutter给开发者提供了一个非常方便的解析库—— dart:convert来帮助开发者进行Json解析的相关操作。

下面,通过梳理平时开发中常用的一些Json数据格式,来一起看下如何使用dart:convert库来进行Json解析。

dart:convert

首先,来了解下dart:convert库的基本使用,首先需要引入这个库,代码如下所示。

import 'dart:convert' show json;

import中使用show关键字表示这里只引入一部分库代码,即引入json相关的代码。接下来,直接通过decode函数,传入json_data数据即可解析,解析返回的数据为Map,key为String类型,value为dynamic类型,类似于Java中的Object类型,这点很好理解,因为JsonObject的Value可以为多种类型,例如String、int等等,所以这里只会返回dynamic类型。

Map<String, dynamic> decodeJson = json.decode(json_data);

有了返回的Map之后,就可以直接解析Map来获得需要的数据了,这里通过一个Text组件简单的展示返回的数据,整个代码如下所示。

import 'dart:convert' show json;

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Json'),
      ),
      body: Column(
        children: <Widget>[
          Container(
            margin: EdgeInsets.all(16),
            child: FutureBuilder(
              future: DefaultAssetBundle.of(context)
                  .loadString("assets/basicMap.json"),
              builder: (context, snapshot) {
                if (snapshot.hasData) {
                  Map<String, dynamic> decodeJso
                                     = json.decode(snapshot.data);
                  return Text(decodeJson.toString());
                } else {
                  return Text('loading...');
                }
              },
            ),
          )
        ],
      ),
    );
  }
}

Json数据文件这里放置在Asset中,所以需要通过一个FutureBuilder来做异步加载,DefaultAssetBundle是一个帮助开发者从Asset中读取文件的工具,通过上面这个框架代码,就可以解析Json并展示数据了。

下面是笔者梳理的几种常见的Json数据格式,与在Android中操作Json类似,通常情况下,都会生成一个Json对应的Model来实现对Json的映射,不过在Flutter中,由于不能使用反射,所以不能像Android的Gson那样直接通过Model反射来实现Json-model的序列化。

JsonObject格式

这个应该是最简单的Json数据格式了,最外层是JsonObject,包含kv类型的Json数据,一个常见的示例如下所示。

{  
    "code": 0,
    "result": "success",
    "message": "message ok"
} 

从这个最简单的例子开始,让我们一步步来了解Dart中的Json解析。

首先,与在Android中解析Json一样,创建一个Dart Model来进行映射,代码如下所示。

class BasicMap {
  int code;
  String result;
  String message;

  BasicMap.fromJson(Map<String, dynamic> json)
      : code = json['code'],
        result = json['result'],
        message = json['message'];

  @override
  String toString() {
    return 'BasicModel: code: $code ,result: $result ,message: $message';
  }
}

这个类与在Android中生成的Model非常类似,首先是属性名,其次是构造函数,最后重写了toString来进行展示。这里的重点就在中间的构造函数中,该具名构造函数接受一个Map<String, dynamic>类型的参数,也就是前面提到的通过dart:convert转换出来的数据,并通过构造函数给属性赋值,值就是参数中取出的数据。修改下上面的框架代码,传入json String,就可以直接返回一个Model了,代码如下所示。

Map<String, dynamic> decodeJson = json.decode(snapshot.data);
BasicMap basicModel = BasicMap.fromJson(decodeJson);

到此为止,Dart中的Json解析和Android中的Json解析基本都是一致的。

JsonObject格式_带有数组格式数据

第一种格式中,都是基本数据类型,下面再增加一个数组类型的数据,如下所示,key:data的value是一个String数组。

{
  "code": 0,
  "result": "success",
  "message": "message ok",
  "data": [
    "xuyisheng",
    "zhujia"
  ]
}

仿照第一个的示例,来生成一个Model,代码如下所示。

class BasicMapWithList {
  int code;
  String result;
  String message;
  List<String> data;

  BasicMapWithList.fromJson(Map<String, dynamic> json)
      : code = json['code'],
        result = json['result'],
        message = json['message'],
        data = json['data'];

  @override
  String toString() {
    return 'BasicModel: code: $code ,result: $result
                        message: $message ,data: ${data.toString()}';
  }
}

但运行之后,大家可以发现Json并不能正确的解析,并提示了下面的错误。

type 'List<dynamic>' is not a subtype of type 'List<String>'

问题就出在对String数组的解析上,data属性的类型是List,但dart:convert解析后返回的是List,不同类型的数组之间是不能相互转换的,所以代码报错了。知道了错误原因后再要解决就非常简单了,只需要指定下返回数据的类型即可,修改下构造函数,代码如下所示。

BasicMapWithList.fromJson(Map<String, dynamic> json)
    : code = json['code'],
      result = json['result'],
      message = json['message'],
      data = List<String>.from(json['data']);

通过List.from()函数,将dynamic类型直接转换成了String类型,这样解析就没有问题了。

JsonObject格式_嵌套JsonObject数据

前面的Json数据都不包含嵌套,下面给Json数据增加一层嵌套,data的value是一个JsonObject,代码如下所示。

{
  "code": 0,
  "result": "success",
  "message": "message ok",
  "data": {
    "name": "xuyisheng",
    "age": 18
  }
}

那么针对嵌套的Json数据,首先要从嵌套的最里层逐步向外创建Model,这点和Gson生成Model非常类似。

因此,首先,需要生成Data Model,代码如下所示。

class Data {
  String name;
  int age;

  Data.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        age = json['age'];

  @override
  String toString() {
    return 'name: $name ,age: $age';
  }
}

这个非常简单,和第一部分的处理是一样的。

接下来,再来生成外层的Model,代码如下所示。

class BasicMapWithModel {
  int code;
  String result;
  String message;
  Data data;

  BasicMapWithModel.fromJson(Map<String, dynamic> json)
      : code = json['code'],
        result = json['result'],
        message = json['message'],
        data = json['data'];

  @override
  String toString() {
    return 'BasicModel: code: $code ,result: $result
                        message: $message ,data: ${data.toString()}';
  }
}

运行下代码,你会发现又碰到了类型转换的错误,显然,问题同样出现在data的解析上,json[‘data’]返回的dynamic类型,所以需要转换为Data类型,因此,修改后的代码如下所示。

BasicMapWithModel.fromJson(Map<String, dynamic> json)  
    : code = json['code'],  
      result = json['result'],  
      message = json['message'],  
      data = Data.fromJson(json['data']);

通过类型转换,就完成了解析。

JsonObject格式_带有List类型JsonObject数据

在前一种数据格式的基础上,再进行进一步的嵌套,data中是一个JsonArray,数据如下所示。

{
  "code": 0,
  "result": "success",
  "message": "message ok",
  "data": [
    {
      "name": "xuyisheng",
      "age": 18
    },
    {
      "name": "zhujia",
      "age": 3
    }
  ]
}

对于这样的Json,处理方式和前面的一种基本类似,首先,从里层生成一个最基本的数据Model,可以随便命名,代码如下所示。

class Person {
  String name;
  int age;

  Person.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        age = json['age'];

  @override
  String toString() {
    return 'name: $name ,age: $age';
  }
}

再生成外面的数据Model,代码如下所示。

class BasicMapWithListModel {
  int code;
  String result;
  String message;
  List data;

  BasicMapWithListModel.fromJson(Map<String, dynamic> json)
      : code = json['code'],
        result = json['result'],
        message = json['message'],
        data = (List.from(json['data']
                          .map((i) => Person.fromJson(i)).toList();

  @override
  String toString() {
    return 'BasicModel: code: $code ,result: $result
                        message: $message ,data: ${data.toString()}';
  }
}

这个地方就有点不好理解了,没关系,一步步来,首先,json[‘data’]返回的是一个List,每个元素实际上都是一个Person对象,所以,对于每一个元素,都需要使用Person的fromJson函数来进行转换,借助List的map操作符,就可以非常简单完成这一操作,经过转换之后,就完成了数据的解析工作。

JsonArray格式

前面几种Json数据格式最外层都是JsonObject,而下面这种,最外层就是一个JsonArray,数据如下所示。

[
  {
    "name": "xuyisheng",
    "age": 18,
    "sex": "M"
  },
  {
    "name": "zhujia",
    "age": 3,
    "sex": "F"
  }
]

对于这样的Json解析,可以参考下Android中的Json解析,首先,可以在最外面封装一个数据Model,其属性就是一个包含上面数据结构的List,首先,还是创建里层的数据Model,代码如下所示。

class Family {
  String name;
  String sex;
  int age;

  Family.fromJson(Map<String, dynamic> json)
      : name = json['name'],
        sex = json['sex'],
        age = json['age'];

  @override
  String toString() {
    return 'name: $name ,age: $age ,sex: $sex';
  }
}

接下来,创建外层的数据Model,代码如下所示。

class BasicList {
  List<Family> families;

  BasicList.fromJson(List<dynamic> json)
      : families = json.map((i) => Family.fromJson(i)).toList();

  @override
  String toString() {
    return 'BasicList: $families';
  }
}

这里要注意的是,构造函数接受的参数为List而非之前的Map,原因就是这种结构的Json通过dart:convert转换出来的是List。
总结
上面列举的Json类型基本上已经涵盖了平常开发中所遇到的Json数据格式,总结下Dart中进行Json转换的一般步骤。

  1. 确定最外层返回的是List还是Map
  2. 从最里层开始向外创建数据Model
  3. 对特定类型的数据进行数据类型转换

注意,有些开发者在创建数据Model的时候喜欢使用工厂函数,例如下面的代码。

BasicMapModel({this.code, this.result, this.message});
factory BasicMapModel.fromJson(Map<String, dynamic> json) {
  return BasicMapWithListModel(
    code: json['code'],
    result: json['result'],
    message: json['message'],
  );
}

这种方式与本文所采用的方式并没有什么区别,只不过工厂函数可以更加灵活的控制实例的产生方式,所以在某些情况下,工厂函数会更加灵活,不过大部分情况下,使用普通的具名函数来创建实例就已经够了。

json_serializable

相比Android中的Json解析,Flutter的解析解析显得有些原始,原因在于Flutter不支持反射,所以无法像Gson那样通过反射来生成Json对象。

不过,回过头来想想在Flutter中的Json解析步骤,首先,需要把Json格式的字符串抽象成数据实体Model,这和在Android中使用Gson的步骤是一样的,只不过在Flutter中,多了一步生成fromJson函数的过程,而这个函数是非常简单的硬编码,即手动解析每个需要的字段,所以,这个过程也是可以通过脚本来自动化完成的,Flutter的开发团队也意识到了这一点,所以开发了json_serializable来帮助开发者完成这些繁杂的体力劳动。

配置json_serializable

首先,打开pubspec.yaml文件,增加json_annotation、build_runner和json_serializable的配置,如下所示。

dependencies:
  flutter:
    sdk: flutter

  json_annotation: ^2.2.0

dev_dependencies:
  flutter_test:
    sdk: flutter

  build_runner: ^1.3.3
  json_serializable: ^2.2.1

这里要注意的是,build_runner和json_serializable是放在dev_dependencies中的,它类似于Android中的debugCompile,也就是只在编译的时候使用,Release中是不会有这两个库的,它们仅仅是帮助开发者来生成代码。
这些库的最新版本,可以从Dart Pub上查找。

配置好之后,点击Android Studio上的Packages get、Packages upgrade或者在命令行中来执行这些指令来获取这些引用库。

示例

下面就通过一个例子来演示下如何使用json_serializable,首先,找到一个之前使用的Json,如下所示。

{
  "code": 0,
  "result": "success",
  "message": "message ok",
  "data": {
    "name": "xuyisheng",
    "age": 18
  }
}

这个Json,前面已经设置好了数据实体,如下所示。

class BasicMapWithModel {
  int code;
  String result;
  String message;
  Data data;
}

class Data {
  String name;
  int age;
}

接下来,为了使用这个库,还需要给这个类添加一些代码,分为下面几个步骤。

首先,引入相关的依赖,代码如下所示。

import 'package:json_annotation/json_annotation.dart';

part 'TestJsonSerializable.g.dart';

TestJsonSerializable.g.dart文件就是稍后需要自动生成的文件,它的命名方式就是『数据实体的文件名.g.dart』。

接下来,给每个实体类增加注解,build_runner就是通过这个注解来寻找需要生成代码的类,如下所示。

@JsonSerializable() 

然后给每个实体增加构造函数、fromJson和toJson函数,这里要注意的是,fromJson和toJson函数不需要具体实现,只需要生成函数名即可,具体的手动解析Json的过程,就是build_runner来生成的,代码如下所示。

TestJsonSerializable(this.code, this.result, this.message, this.data);  

factory TestJsonSerializable.fromJson(Map<String, dynamic> srcJson) => 
    _$TestJsonSerializableFromJson(srcJson);  

Map<String, dynamic> toJson() => _$TestJsonSerializableToJson(this); 

以最外层的Model为例,这里生成文件名的规则是『$数据实体类名FromJson』、『$数据实体类名ToJson』。全部的代码如下所示。

import 'package:json_annotation/json_annotation.dart';

part 'TestJsonSerializable.g.dart';

@JsonSerializable()
class TestJsonSerializable extends Object {
  int code;
  String result;
  String message;
  Data data;

  TestJsonSerializable(this.code, this.result, this.message, this.data);

  factory TestJsonSerializable.fromJson(Map<String, dynamic> srcJson) =>
      _$TestJsonSerializableFromJson(srcJson);

  Map<String, dynamic> toJson() => _$TestJsonSerializableToJson(this);
}

@JsonSerializable()
class Data extends Object {
  String name;
  int age;

  Data(this.name, this.age);

  factory Data.fromJson(Map<String, dynamic> srcJson) =>
      _$DataFromJson(srcJson);

  Map<String, dynamic> toJson() => _$DataToJson(this);
}

在编写这个代码的时候,由于需要的代码还没生成,所以编译器会报红警告,这是正常的,当代码生成后,这些警告自然就没有了。

最后,通过运行build_runner来生成所需要的代码,命令如下所示。

➜  flutter_json flutter packages pub run build_runner build

在项目目录下执行上面的指令即可,生成过程如下图所示。

➜  flutter_json flutter packages pub run build_runner build
[INFO] Generating build script...
[INFO] Generating build script completed, took 423ms

[INFO] Initializing inputs
[INFO] Reading cached asset graph...
[INFO] Reading cached asset graph completed, took 127ms

[INFO] Checking for updates since last build...
[INFO] Checking for updates since last build completed, took 977ms

[INFO] Running build...
[INFO] 1.0s elapsed, 0/1 actions completed.
[INFO] Running build completed, took 1.1s

[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 61ms

[INFO] Succeeded after 1.1s with 2 outputs (3 actions)

指令执行成功后,就会在实体类的当前目录下生成前面定义的『.g.dart』的文件。打开该文件,如下所示。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'TestJsonSerializable.dart';

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

TestJsonSerializable _$TestJsonSerializableFromJson(Map<String, dynamic> json) {
  return TestJsonSerializable(
      json['code'] as int,
      json['result'] as String,
      json['message'] as String,
      json['data'] == null
          ? null
          : Data.fromJson(json['data'] as Map<String, dynamic>));
}

Map<String, dynamic> _$TestJsonSerializableToJson(
        TestJsonSerializable instance) =>
    <String, dynamic>{
      'code': instance.code,
      'result': instance.result,
      'message': instance.message,
      'data': instance.data
    };

Data _$DataFromJson(Map<String, dynamic> json) {
  return Data(json['name'] as String, json['age'] as int);
}

Map<String, dynamic> _$DataToJson(Data instance) =>
    <String, dynamic>{'name': instance.name, 'age': instance.age};

这些代码就是build_runner自动生成的解析Json的代码,至此,解析所需要的实体类就创建好了,现在回过头来看看,与之前手动解析Json自己写的那些方法,基本都是一样的,只不过这些机械的代码被build_runner自动生成了而已。

另外,build_runner也支持对文件的监听,来自动创建新生成的数据实体类,指令如下所示。

flutter packages pub run build_runner watch

进阶

json_annotation是类似Gson的注解,除了前面提到的@JsonSerializable()注解,其实还有很多其它注解,例如:@JsonKey(name="Json_Name"),即获取指定Json名的字段,类似Gson中的@SerializedName("Json_Name ")。更多的参数可以在json_annotation的Github上找到,这里不进行进一步的解释了,都是一些配置参数。

简化

虽然说官方提供了json_annotation、build_runner和json_serializable来简化Json解析实体类的生成,但是这个使用过程还是非常繁琐的,特别是创建好最简单的实体类后,还需要补充各种依赖、fromJson函数等内容,其实还是非常不方便的,但好在这些过程实际上也是非常机械的,同样可以通过一些方式来进行简化,Android Studio的Live Templates就是一个不错的选择,创建一个Live Templates,并增加要自动插入的代码,如图所示。

模板代码示例如下所示。

import 'package:json_annotation/json_annotation.dart';

part '$NAME$.g.dart';

@JsonSerializable()
class $NAME$ extends Object {

    String $V1$;
    String $V2$;
    
    $NAME$(this.$V1$, this.$V2$);
    
    factory $NAME$.fromJson(Map<String, dynamic> srcJson) =>
    _$$$NAME$FromJson(srcJson);
    
    Map<String, dynamic> toJson() => _$$$NAME$ToJson(this);
}

这就是一个最简单的数据实体类的模板代码了,在编辑器中输入创建的快捷键,就可以非常方便的使用这个快捷代码了,创建好之后,再通过build_runner就可以创建所需要的其它文件了。

这里需要注意的是,『$』在模板代码中表示的是变量,但这里需要将它作为文本处理,所以查阅idea的文档后发现,可以通过『$$』来进行转义,相关文档如下。

https://www.jetbrains.com/help/idea/template-variables.html

template1234

进一步简化

相比在Android中使用GsonFormat进行Json的实体类生成,Flutter中生成实体类的方式还是有些麻烦,因此,如果能够将GsonFormat的源码进行改造,实际上是完全可以直接通过Json生成实体类的。

当然,类似的工具还有很多,毕竟Json to Model在各个平台中都是需要的,类似的还有下面这个工具。
https://caijinglong.github.io/json2dart/index_ch.html

再例如下面这个工具,如图所示。
https://javiercbk.github.io/json_to_dart/

这些工具都可以完成这样的操作。

真·总结

本文从最基础的Flutter Json解析到一步步更加复杂的Json解析,再到更加高效的Json解析,一点点的让开发者了解如何在Flutter中处理Json。之所以没有直接讲解最高效的使用方法,是为了让开发者对Flutter中的Json解析有一个比较完整和深入的理解,这样在使用这些工具的时候才能知其所以然。