如何在Flutter中解析JSON字符串?

361 阅读9分钟

如果你要与一个API进行交互,你认为响应会是什么格式?它可能--或者几乎肯定--是JSON格式。

JSON是将键入的数据从一个点传输到另一个点的标准。如果你查询API,或者甚至想在你的应用程序中保存设置,你迟早都要使用JSON。

JSON中的序列化和反序列化

在处理JSON时,有两个主要动作可以发生:

  • 序列化 - 将一个数据对象从其母语类型转换为JSON字符串
  • 反序列化--从JSON字符串转换为本地对象

实际上,这是我们现在就可以做的事情,就在你用来阅读这篇文章的那个浏览器中。如果你按下键盘上的F12键,进入开发者控制台,你可以输入以下内容:

JSON.stringify({serialization: 'fun'})

这样做的结果是:'{"serialization":"fun"}'

好的,那么刚才发生了什么?我们创建了一个内存中的JavaScript对象,然后立即通过JSON.stringify 将该对象转换为JSON。我们可以看到它是一个字符串,因为它被引号所包围。

我们可以通过运行以下程序来反向运行这个过程:

JSON.parse('{"serialization":"fun"}')

这样做的结果如下:

Converting Between A Json String And A Javascript Object With JSON.parse

现在,我们要把这个对象的字符串表示转换回真正的JavaScript对象。由于JavaScript是一种非类型语言,我们不需要指定我们想要序列化或反序列化的JSON字符串的类型。

在一个类型化的语言中,比如C#,从对象到字符串的序列化和反序列化是类似的。然而,有一点不同的是,当我们反序列化时,我们需要通过以下方式指定我们希望反序列化的类型:

JsonConvert.DeserializeObject<T>

在上面的命令中,T 是我们试图反序列化的目标类型。

也许你会认为在Flutter中序列化数据这样简单而普遍的事情会像在C#或JavaScript中完成同样的事情一样容易。不幸的是,情况并非如此,但这种挑战是有原因的。

为什么在Flutter中解析JSON是一个挑战?

在Flutter中序列化和反序列化JSON字符串与其他语言的关键区别在于Flutter不支持被称为 "反射 "的运行时功能。通过反射,其他语言能够检查对象的信息,帮助对象序列化。

然而,Flutter缺乏反射实际上是一件好事。没有反射,Flutter代码避免了相关的性能注意事项,并为终端用户带来了更小的可安装二进制文件。

我们仍然可以在Flutter中轻松解析JSON字符串,但我们需要做的不仅仅是指定一个类型。幸运的是,我们可以生成完成这一任务所需的所有代码,因此我们不必手工编写。

当你想在Flutter中序列化数据时,有两种情况可以发生:

一种情况是,你自己控制着数据类,你想把你的数据转换为JSON,或从JSON中转换出来,发送到API或存储在设备上。

在另一种情况下,你自己不控制数据类,而从API接收JSON。你想把这个JSON转换成强类型的数据,你可以在你的Flutter应用程序中操作。

在这篇文章中,我们将考虑这两种情况。首先,我们将看看当我们控制数据时,如何在应用中序列化一个类。然后,我们将回顾在Flutter中从远程来源反序列化JSON数据。

在 Flutter 应用程序中用 JSON 序列化一个类

让我们想象一下,我们有一个音乐应用程序,用户可以在其中设置歌曲作为他们的最爱。为了实现这一目的,我们需要一个名为Favorite 的类。在这个类中,我们通常会有一些属性,如歌曲的标题和艺术家。

最终的结果会是这样的:

class Favorite {
  final String title;
  final String artist;

  Favorite(this.title, this.artist);
}

那么,我们如何在Flutter中对这样的类进行序列化和反序列化?你可以自己做,或者自动生成JSON处理代码。让我们来看看这两种选择。

定义您自己的toJsonfromJson 方法

如果你只有几个想序列化或反序列化的类,可以选择简单地自己做。这意味着定义你自己的toJsonfromJson 方法,这些方法负责将数据从Map<String, dynamic> 转换回原始数据模型。

如果我们这样做,我们的类就会是这样的:

class Favorite {
  final String title;
  final String artist;

  Favorite(this.title, this.artist);

  /// Convert from JSON response stream to Favorite object
  Favorite.fromJson(Map<String, dynamic> json)
      : title = json['title'],
        artist = json['artist'];

  /// Convert an in-memory representation of a Favorite object to a Map<String, dynamic>
  Map<String, dynamic> toJson() => {
        'title': title,
        'artist': artist,
      };
}

然后我们可以用jsonEncodejsonDecode ,分别使用我们的fromJsontoJson 方法。但在Flutter中定义自己的JSON数据转换方法有一些缺点。

例如,如果我们曾经更新了类的成员,我们将不得不手动更新属性。我们也有可能参考了错误的成员,或者只是打错了字,试图意外地映射一个不存在的属性。

这种方法也为我们在Flutter应用程序中处理JSON增加了很多开销。使用现有的包来生成JSON序列化和反序列化代码会更方便。

自动生成您的JSON处理代码

官方Flutter文档说,对于较小的应用程序,您可以手工编写自己的JSON映射代码。在现实中,你总是更好地生成JSON序列化和反序列化代码。

自动生成JSON处理代码的包都是经过严格测试的,并且像Flutter一样已经存在了很长时间,所以你知道它们是有效的。

这些包中的一个是 [json_serializable](https://pub.dev/packages/json_serializable).在我们的Flutter代码中使用一些简单的属性,我们可以在短时间内为JSON序列化生成我们的代码。

首先,将json_serializable 添加到你的pubspec.yaml 文件中,作为dependency ,也作为dev_dependency 。我们还需要添加对build_runner 的引用,因为json_serializable 使用build_runner 来为我们创建功能。

接下来,用@JsonSerializable 来注释我们的类。这将指示json_serializable 包生成我们需要的代码,以便在Flutter中把这个类序列化成JSON。

我们还想在这个文件的顶部添加part 指令,并指定favorite.g.dart 。当我们最初写这个的时候,我们会在下面看到红色的方格,因为这个文件还没有被创建。但是我们需要在我们的文件中定义这个,以使json_serializable 包能够工作。

part 'favorite.g.dart'

@JsonSerializable
class Favorite {
  final String title;
  final String artist;

  Favorite(this.title, this.artist);
}

有了这些变化,现在我们可以在终端运行以下命令:

flutter pub run build_runner build

在一些控制台输出后,我们就能看到我们的favorites.g.dart 文件出现在我们的项目中。该文件的内容如下:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'favorite.dart';

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

Favorite _$FavoriteFromJson(Map<String, dynamic> json) => Favorite(
      json['title'] as String,
      json['artist'] as String,
    );

Map<String, dynamic> _$FavoriteToJson(Favorite instance) => <String, dynamic>{
      'title': instance.title,
      'artist': instance.artist,
    };

不幸的是,json_serializable 并没有在生成的代码中直接暴露这些toJsonfromJson 的功能。这意味着我们要把它们包含在我们的原始文件中。总而言之,我们最终的favorite.dart 文件应该是这样的:

import 'package:json_annotation/json_annotation.dart';

part 'favorite.g.dart';

@JsonSerializable()
class Favorite {
  final String title;
  final String artist;

  Favorite(this.title, this.artist);

  factory Favorite.fromJson(Map<String, dynamic> json) => _$FavoriteFromJson(json);

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

当我们想对Favorite 对象进行序列化或编码时,我们使用以下命令:

final favoriteJson = jsonEncode(Favorite('one', 'two'));

如果我们想把这个基于文本的JSON转换回原始对象,我们可以做以下工作:

final favorite = Favorite.fromJson(jsonDecode(favoriteJson));

这就是我们在自己的应用程序中控制对象时管理JSON的方法。但当我们不控制数据类时,我们该怎么做呢?例如,如果我们正在查询一个我们没有数据结构的远程API?

在 Flutter 中从远程来源反序列化 JSON 数据

如果我们有一个远程API,我们可以使用一个在线工具,将提供的JSON转换为我们可以在我们的应用程序中使用的类,其中包括toJsonfromJSON 方法。我们要用来演示的一个工具是Github上的一个JSON到Dart的转换器

在这个例子中,我们将使用Bored API,它可以在我们感到无聊的时候随机给我们一个任务做。Bored API以JSON格式响应,很适合这个目的。由于它是随机的,你收到的任务将与我们下面使用的例子不同。

在我的例子中,我从API收到的响应是如下:

{"activity":"Go for a walk","type":"relaxation","participants":1,"price":0,"link":"","key":"4286250","accessibility":0.1}

要在我们的Flutter应用程序中序列化和解析这些JSON数据,只需将其粘贴到JSON到Dart转换器中,如下图所示:

Json To Dart Converter With Json Data Pasted In And Generated BoredAPI Code

现在,我们只需将BoredAPI 类复制并粘贴到我们的应用程序中就可以了。在这种情况下,我们为Bored API生成的代码会是这样的:

class BoredAPI {
  String? activity;
  String? type;
  int? participants;
  int? price;
  String? link;
  String? key;
  double? accessibility;

  BoredAPI(
      {this.activity,
      this.type,
      this.participants,
      this.price,
      this.link,
      this.key,
      this.accessibility});

  BoredAPI.fromJson(Map<String, dynamic> json) {
    activity = json['activity'];
    type = json['type'];
    participants = json['participants'];
    price = json['price'];
    link = json['link'];
    key = json['key'];
    accessibility = json['accessibility'];
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['activity'] = this.activity;
    data['type'] = this.type;
    data['participants'] = this.participants;
    data['price'] = this.price;
    data['link'] = this.link;
    data['key'] = this.key;
    data['accessibility'] = this.accessibility;
    return data;
  }
}

我们可以看到,我们有所有的类成员。我们也声明了fromJsontoJson 函数。另外,我们生成的代码支持空安全,所以我们可以在更多的Flutter版本中使用它。

有了这些,我们现在可以安全地与这个API交互,并根据需要在Flutter中对JSON数据进行编码和解码。

总结

在Flutter中解析JSON字符串比在其他语言中要困难一些。正如我们所看到的,这是由于Flutter内缺乏运行时的反射。

然而,在我看来,Flutter通过让我们根据需要生成适用的类来序列化我们的JSON,从而弥补了这些小的烦恼。

我们还看到,即使我们不拥有进入我们应用程序的数据,我们也可以通过为从API收到的响应生成JSON类来创建转换为JSON的功能。

如果你正在与API进行交互,并且对JSON进行序列化,你也可以考虑使用像OpenAPI或Swagger这样的东西来为你生成所有的API代码。这样,你就可以只处理API功能,而不是手动将你的对象转换为JSON。