Day27 JSON and serialization

这几天我们来介绍一些工具,在之後实作专案时会很常用到

我们的app 常常需要一些来自後台的资料,像是与Web 服务器进行通信来传递资讯,而这些数据传输方式基本都是用JSONJSON是我们开发中最常使用的一种资料格式,官方文件

序列化 serialization

我们通常会将需要发送的数据序列化为JSON格式的字串流结构化资料进行传输,而反序列化则是将从获得的JSON格式字串流结构化资料进行反序列化,重建我们所要的资料结构数据

JSON 序列化数据的方法

  1. 手动序列化数据

    规模较小的专案可以使用手动序列化

    使用Flutter 内建的dart:convert的库,这个library 包含了一个简单的JSON编码器和解码器

    例如:

    import 'dart:convert';
    
    void main() {
      String jsonString = ''' {
        "name": "John Smith",
        "email": "[email protected]"
      } ''';
    
      manualDecode(jsonString);
    }
    
    void manualDecode(String jsonString) {
      Map<String, dynamic> user = jsonDecode(jsonString);
    
      print('Howdy, ${user['name']}!'); //印出 Howdy, John Smith!
      print('We sent the verification link to ${user['email']}.'); //印出 We sent the verification link to [email protected].
    }
    

    然而,jsonDecode()返回一个Map<String, dynamic>,也就是说直到执行时我们才知道值的类型,代表使用这个方法,我们就失去了大部分的静态类型语言特性:类型安全,自动补全以及最重要的编译时异常,例如,当我们要存取name栏位,但是名称却打错了,此时编译器在编译时不会帮忙报错

    解决方法:在Model 类别中序列化JSON

    通过事先定义与Json结构对应的Model类,然後在请求到数据後再动态根据数据创建出Model类的实例

    例如:

    建立一个与上述范例Json 对应的Model 类别,User:

    • 一个User.fromJson建构函数,用於从map 结构资料中构造出一个新的User实例
    • 一个toJson方法,将User实例转化为一个map 结构化资料
    class User {
      final String name;
      final String email;
    
      User(this.name, this.email);
    
      User.fromJson(Map<String, dynamic> json)
          : name = json['name'],
            email = json['email'];
    
      Map<String, dynamic> toJson() =>
        {
          'name': name,
          'email': email,
        };
    }
    

    通过这种方法可以拥有类型安全, nameemail字段的自动补全字段以及编译时异常(检测),如果你发生了笔误或者把String类型的字段看成了int类型,app在编译时就不会通过,而不是在执行时抛出异常

    解码序列化逻辑现在移动到了模型内部,通过此方法可以很容易地解码/反序列化一个 user

    import 'dart:convert';
    
    void main() {
      String jsonString = ''' {
        "name": "John Smith",
        "email": "[email protected]"
      } ''';
    
      Map userMap = json.decode(jsonString);
      var user = new User.fromJson(userMap);
    
      print('Howdy, ${user.name}!');
      print('We sent the verification link to ${user.email}.');
    }
    
    class User {
      final String name;
      final String email;
    
      User(this.name, this.email);
    
      User.fromJson(Map<String, dynamic> json)
          : name = json['name'],
            email = json['email'];
    
      Map<String, dynamic> toJson() => {
            'name': name,
            'email': email,
          };
    }
    

    要编码/序列化 user,将User实例传到jsonEncode()函数中,你不需要调用toJson()方法,因为jsonEncode()已经帮你做了这件事

    String json = jsonEncode(user);
    

    通过这种方法,被调用的代码根本不需要担心序列化JSON数据的问题,然而,模型Model 类别仍然是必须的。在一个生产环境下的App,你可能希望确保序列化数据能正确奏效。所以User.fromJson()User.toJson()方法都需要单元测试以便验证正确的行为

    然而,现实场景通常不是那麽简单,有时候响应的JSON API 会更加复杂,例如它可能会包含一些相邻的JSON 对象,而这些对象同样需要使用它的model 类进行解析,此时我们就需要使用代码来自动生成库序列化JSON 数据

  2. 利用代码自动生成序列化数据

    尽管有其它的library 可以使用,我们来介绍官方推荐的函式库json_serializable,由於序列化数据不再需要手动编写或者维护,你可以将序列化JSON 数据在运行时的异常风险降到最低

    添加依赖:

    ...
    dependencies:
      flutter:
        sdk: flutter
    
      cupertino_icons: ^1.0.0 
      json_annotation: ^3.1.0 #当前版本
    
    dev_dependencies:
      flutter_test:
        sdk: flutter
    
      build_runner: ^1.10.3 #当前版本
      json_serializable: ^3.5.0 #当前版本
    ...  
    

    以 json_serializable 的方式创建model类

    user.dart

    import 'package:json_annotation/json_annotation.dart';
    
    /// This allows the `User` class to access private members in
    /// the generated file. The value for this is *.g.dart, where
    /// the star denotes the source file name.
    part 'user.g.dart';
    
    /// An annotation for the code generator to know that this class needs the
    /// JSON serialization logic to be generated.
    @JsonSerializable()
    
    class User {
      User(this.name, this.email);
    
      String name;
      String email;
    
      /// A necessary factory constructor for creating a new User instance
      /// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
      /// The constructor is named after the source class, in this case, User.
      factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
    
      /// `toJson` is the convention for a class to declare support for serialization
      /// to JSON. The implementation simply calls the private, generated
      /// helper method `_$UserToJson`.
      Map<String, dynamic> toJson() => _$UserToJson(this);
    }
    

    有了这些设置,程序码生成器就会生成用於从JSON中编码和解码nameemail这两个栏位的程序码

    如果需要,自定义命名策略也很容易。例如,如果我们正在使用的API返回带有snake_case的对象,但我们想在我们的模型中使用lowerCamelCase,那麽我们可以使用@JsonKey标注:

    /// Tell json_serializable that "registration_date_millis" should be
    /// mapped to this property.
    @JsonKey(name: 'registration_date_millis')
    final int registrationDateMillis;
    

    当你首次创建json_serializable类时,你会得到类似下图的错误

    https://ithelp.ithome.com.tw/upload/images/20201012/20118479ceymkCE0PW.png

    这些错误是完全正常的,这是因为Model类的生成代码还不存在。为了解决这个问题,我们必须运行代码生成器来为我们生成序列化模板。有两种运行代码生成器的方法:

    1. 一次性生成

      透过在专案的根目录下执行flutter packages pub run build_runner build,这触发了一次性构建,我们可以在需要时为我们的Model生成json序列化代码,它通过我们的源文件,找出需要生成Model类的源文件(包含@JsonSerializable标注的)来生成对应的.g.dart文件。一个好的建议是将所有Model 类放在一个单独的目录下,然後在该目录下执行命令

      虽然这非常方便,但如果我们不需要每次在Model类中进行更改时都要手动运行构建命令的话会更好

    2. 持续生成

      使用watcher可以使我们的源代码生成的过程更加方便。它会监视我们项目中文件的变化,并在需要时自动构建必要的文件,我们可以通过flutter packages pub run build_runner watch在项目根目录下运行来启动watcher。只需启动一次观察器,然後它就会在後台运行,这是安全的

    使用json_serializable,在User类中你可以忘记所有手动序列化的JSON数据。源代码生成器会创建一个名为user.g.dart的文件,它包含了所有必须的序列化数据逻辑。你不必再编写自动化测试来确保序列化数据奏效。现在由库来负责确保序列化数据能正确地奏效

为嵌套类(Nested Classes) 生成代码

你可能在代码中用了嵌套类,在你把类别作为参数传递给一些服务(比如Firebase)的时候,你可能会遇到Invalid argument错误

比如下面的这个Address类:

import 'package:json_annotation/json_annotation.dart';
part 'address.g.dart';

@JsonSerializable()
class Address {
  String street;
  String city;

  Address(this.street, this.city);

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

一个Address类被嵌套在User类中使用:

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable()
class User {
  String firstName;
  Address address;

  User(this.firstName, this.address);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

在终端机中运行flutter pub run build_runner build创建* .g.dart文件,但私有函数如_ $ UserToJson()会看起来像下面这样

user.g.dart

(
Map<String, dynamic> _$UserToJson(User instance) => <String, dynamic>{
  'firstName': instance.firstName,
  'address': instance.address,
};

看起来没有什麽问题,但如果print User 实例时:

import 'package:json_tutorial/user.dart';

import 'address.dart';

void main() {
  Address address = Address("My st.", "New York");
  User user = User("John", address);
  print(user.toJson()); // 印出 {firstName: John, address: Instance of 'Address'}
  //不是我们要的结果:{name: John, address: {street: My st., city: New York}}
}

为了得到正常的输出,你需要在类别声明之前为@JsonSerializable方法加入explicitToJson: true参数

user.dart

import 'address.dart';
import 'package:json_annotation/json_annotation.dart';
part 'user.g.dart';

@JsonSerializable(explicitToJson: true)
class User {
  String firstName;
  Address address;

  User(this.firstName, this.address);

  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

main.dart

import 'package:json_tutorial/user.dart';

import 'address.dart';

void main() {
  Address address = Address("My st.", "New York");
  User user = User("John", address);
  print(user.toJson()); // 印出 {firstName: John, address: {street: My st., city: New York}}
}

<<:  Day 27 重构是否要排进待办清单里

>>:  [Day 29] 部属(heroku)

[Day14] CH09:寻寻觅觅——二元搜寻法

接下来的这几天,会疯狂运用到上个单元教的阵列,也会碰触一些演算法的概念,而今天要来介绍的是二元搜寻法...

[D26] 物件侦测(7)

YOLO 是一个不断改进和优化的物件侦测系列,除了前三个版本,在 2020 年时,YOLOv4 也问...

[PoEAA] Domain Logic Pattern - Table Module

本篇同步发布於个人Blog: [PoEAA] Domain Logic Pattern - Tabl...

【Day25】React Class Component 生命周期简单介绍

在写React的时候其实有分为两种写法 Class Component this.state or ...

如何开展你的分析?

今天要和来大家说明一下分析的基本框架要如何展开。这边提供的是一套思考的流程,提醒大家展开分析的过程中...