前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者搜索”IT工匠“关注微信公众号/头条号(微信公众号和头条号同名),会同步推送)。
概述
现在大多数app
都需要与Web
服务器进行通信,而要与Web
服务器端进行通信,就必须选择一种文本格式进行传输,当前用到最多的可能是JSON
格式,我们通常会将需要发送的数据序列化为JSON
格式的字符流进行传输,本文主要介绍如何正确地在Flutter
中使用JSON
。
术语解释:编码和序列化是一个东西,二者都是指将数据结构转换为字符串(序列化的定义1)。解码和反序列化是编码和序列化的相反过程:将字符串转换为数据结构。但是,序列化通常也指将数据结构转换为更易于阅读的格式的整个过程(序列化的定义2)。
为避免混淆,本文中提到的**“序列化”指的是序列化的定义2**。
我们应该选择哪种JSON序列化方法?
本文主要介绍JSON
序列化的两种方法:
- 手动序列化
- 使用代码进行自动序列化
不同的项目具有不同的复杂性和场景,对于小项目或者示例项目来说,使用代码自动序列化显得大材小用,对于复杂度较高的JSON
实例(比如有较多层次的嵌套),手动序列化就会显得很繁杂,而且容易出现错误。
在小项目中进行手动序列化
手动进行JSON
的解码是指使用dart:convert
包内置的JSON
解码器进行解码。做法是将原始JSON
字符串传递给jsonDecode()
函数,然后在该函数的返回值(返回值类型为Map <String,dynamic>
)中查找所需的值。这种方法不需要任何外部依赖,比较适合在小项目或者复杂度较低的应用场景下使用。
当项目变大时,手动解码就会暴露出其缺点:手动编写解码逻辑可能很容易出现错误,如果由于拼写错误等原因试图访问JSON
中不存在的字段,代码会在运行时抛出异常从而导致应用崩溃。
如果你的项目中没有很多JSON
模型,并且应用常见比较简单,那么手动序列化会比较适合你。
在大型项目中使用代码自动序列化
使用代码自动进行JSON
序列化是指我们使用外部库进行编码样板的生成,通过一些初始设置后,就可以使用外部库进行模型类代码的生成,json_serializable
和built_value
是我们比较经常用到的外部库。
这种方法适用于较大的或者是应用场景比较复杂的项目,特点是不需要手动编写的样板文件,并且可以在编译时捕获可能存在的JSON
字段的错误访问。当然这种方法也是有缺点的:它需要一些初始设置,而且外部库生成的源文件会造成我们项目的结构视图显得有点混乱,强迫症选手可能会有点心痛。
这种方法我会在下面进行实例讲解。
两种序列化方法实践
手动序列化JSON
Flutter
中实现基本的JSON
序列化非常简单,Flutter
有一个内置dart:convert
库,其中包含一个简单的JSON
编码器和解码器。
以下是一个JSON
字符串:
{
"name": "John Smith",
"email": "john@example.com"
}
基于dart:convert
包,我们可以用两种方式来将这个字符串解析为我们的变量:
- 内联序列化
JSON
- 在模型类中序列化
JSON
内连序列化JSON
我们可以通过调用JSON.decode()
方法来解码JSON
字符串 ,只需要将待解析的JSON字符串作为该方法的参数传入进去即可:
Map<String, dynamic> user = JSON.decode(json);
print('Howdy, ${user['name']}!');
print('We sent the verification link to ${user['email']}.');
这种方法的缺点是,JSON.decode()
的返回值类型是Map<String, dynamic>
的,这意味着我们直到运行时才能知道值的类型,这样我们失去了大部分静态语言的特性:类型安全、自动补全和最重要的编译时异常检测。
这里解释一下这个编译时异常检测:一般来说异常/错误会发生在两个地方:编译时、运行时。如果错误是在编译时发生,我们可以很容易地定位到出现错误的位置并及时改正,但是如果异常发生在运行时,就会在对应异常被触发时导致整个应用崩溃退出,而且较难排查异常原因。所以,最好的场景是尽可能将可能出现的异常解决在编译阶段,而等运行时出现异常才去解决。
例如,当我们试图访问name
或email
字段时,一不小心将字段名打错了,由于这个解码后的JSON
在存储在map
结构中,编译器不可能再编译时检测到我们字段名的输入错误,所以编译时不会出错,但是运行时会出现异常,甚至导致整个应用程序崩溃退出,用户体验很不好。
在模型类中序列化JSON
我们可以通过引入一个简单的模型类(model class
)来解决上一小节提到的问题,以上一小节的实例来说,我们可以引入一个称之为User
的模型类。在User
类内部,我们有:
- 一个
User.fromJson()
构造函数, 用于从一个map
构造出一个User
实例(对应反序列化/解码) - 一个
toJson()
方法, 用于将User
实例转化为一个map
(对应序列化/编码)
代码在执行的时候可以具有类型安全、自动补全字段(name
和email
)以及编译时异常检测。如果我们使用了拼写错误的字段,我们的应用程序会在编译时报错,而不是在运行时使应用程序崩溃。
具体的代码实现是这样的:
user.dart
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
进行解码:
Map userMap = JSON.decode(json);
var user = new User.fromJson(userMap);
print('Howdy, ${user.name}!');
print('We sent the verification link to ${user.email}.');
要序列化/编码一个user
,我们需要将需要序列化的User
对象传递给JSON.encode()
方法,不需要手动调用user.toJson()
这个方法,因为JSON.encode(user)
底层会为我们调用user.toJson()
方法:
String json = JSON.encode(user);
以上就是两种手动序列化JSON
的实现方法,这种方法在应用场景简单的情况下可以很好地实现我们的序列化需求,但是如果出现像多层JSON
嵌套这样的复杂应用场景,上述方法就显得有点捉襟见肘了,所以我们需要了解如何使用代码生成库来进行JSON
的序列化,这样可以大大提高生产力,Let's Go!
使用代码生成库序列化JSON
FLutter有不止一种代码生成库可以使用,在本例中,我们使用json_serializable
这个库,它是Flutter官方推荐的一个序列化的库,主要的功能是自动化生成序列化源代码(核心在于为我们自动生成上面提到的fromJson()
构造方法和toJson()
方法)。
在项目中引入json_serializable依赖
要将json_serializable
库包含到我们的项目中,我们需要一个常规依赖项(dependencies
)和两个开发依赖项(dev_dependencies
),很多人可能不清楚常规依赖项和开发依赖项的区别,一句话解释就是我们最终发布的应用程序源代码中是不包含开发依赖项的,但是包含常规依赖项。
我们可以在https://github.com/dart-lang/json_serializable/blob/master/example/pubspec.yaml
查看当前依赖库的最新版本,我写这篇文章的时候最新版本如下:
pubspec.yaml
dependencies:
json_annotation: ^2.4.0
dev_dependencies:
build_runner: ^1.0.0
json_serializable: ^3.0.0
完成pubspec.yaml文件中的依赖项添加之后,可以在你项目的根目录下运行 flutter packages get
或者在编辑器中点击 **“Packages Get”**以将这些这些依赖项的必要资源下载到本地。
基于json_serializable创建model类
让我们看看如何将我们的User
类转换为一个json_serializable
。为了简单起见,我们使用前面示例中的简化JSON model。
user.dart
import 'package:json_annotation/json_annotation.dart';
// user.g.dart这个文件现在还没有(所以会报错,但是不要怂,就是刚),但是直接按照这个格式写就好,在一会运行完生成命令之后这个文件会自动生成
part 'user.g.dart';
///这个注解是告诉生成器,这个类是需要生成Model类的
@JsonSerializable()
class User{
User(this.name, this.email);
String name;
String email;
//不同的类使用不同的mixin即可,注意格式一定要写正确
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
Map<String, dynamic> toJson() => _$UserToJson(this);
}
完成了上述代码中的配置后,源码生成器将会为我们生成用于序列化name
和email
字段的JSON
代。需要说明的是,如果像上面那样配置,默认最后生成的JSON
字符串中的键为name
和email
,如果你想自定义命名策略,比如你想让变量名为name
的变量和JSON
字符串中名为MyName
的键对应起来,可以使用@JsonKey
注解来进行实现:
@JsonKey(name: 'MyName')
final String name;
运行代码生成程序
json_serializable
第一次创建类时,您会看到与下图类似的错误。
这些错误是完全正常的,这是因为刚写好这个代码的时候user.g.dart
(该文件存储生成的代码)还不存在,需要我们运行代码生成程序才能生成这个文件,一般来说有两种运行代码生成器的方法:
-
一次性生成
通过在我们项目的根目录下运行
flutter pub run build_runner build
命令,我们可以为模型生成JSON
序列化代码。这个命令会触发一次性构建(one-time build
),构建过程中会遍历源文件,选择需要生成JSON
序列化代码的文件,为它们生成必要的序列化代码。这种做法的优点是可以仅仅通过一条命令搞定代码的生成,缺点是如果我们这次生成完了代码,下次对Modek类进行了一点改动,那么原来生成的代码就不能用了,必须重新运行上述命令进行生成,难免有些麻烦。
-
持续生成
使用
watcher
可以使我们源代码的生成过程更加方便,它会监听我们项目中文件的变化,在必要时自动构建必要的文件。我们可以通过在项目根目录下运行flutter packages pub run build_runner watch
命令来启动watcher
。这样我们只需启动一次watcher
,他就会一直在在后台运行,不存在任何安全隐患。
实现最终的序列化和反序列化
要通过json_serializable
方式反序列化JSON
字符串,我们不需要对先前的代码进行任何修改:
Map userMap = JSON.decode(json);
var user = new User.fromJson(userMap);
序列化也和之前的代码一样:
String json = JSON.encode(user);
有了json_serializable
,我们可以不用在User
类上进行任何手动的JSON
序列化 ,源代码生成器会为我们创建一个名为user.g.dart
的文件,它具有所有必需的序列化逻辑,在提供快捷性的前提下还可以保证不会出错,真是太香了。
Flutter中是否存在GSON/Jackson/Moshiquivalent这些类库?
答案是不存在~
原因在于,这些类库需要使用运行时反射,这在Flutter
中是禁用的。运行时反射会干扰Dart
的tree shaking
。这个tree shaking
的作用是,我们可以在打包程序时去除那些没有实际使用到的代码,这可以显著减小应用程序的大小。而反射会默认使用所有代码,因此tree shaking
就会失灵。
笔者结语
其实在Android
或者其他平台用过像GSON
这类第三方库的人可能都能很明显感觉到Flutter
在JSON
序列化这块的缺点,目前Flutter
还没有一个像GSON
这样简单易用的JSON
序列化解决方案,但是相信随着Flutter
生态的不断完善,会出现一些更好的开源解决方案,如果一直没有,笔者会利用闲余时间自己写一套,届时也将开源在此处,欢迎大家关注~
好了,以上就是本文的所有内容,我们昨天预告本来今天要更新Flutter
动画相关的内容,但是今天梳理了一下动画部分的内容,发现内容很多,所以决定先更新Flutter
中最核心的部分,至于动画这些加分技能会在更新完核心部分后逐步更新,欢迎大家持续关注,最后预告一下我们明天的更新内容:明天我会向大家介绍如何在Flutter
中实现平台特定代码的编写,比如Flutter
如何与Android/Ios
平台的代码进行通信、Flutter
如何访问平台特定API
等,我们明天见~