极客时间已完结课程限时免费阅读

24 | HTTP网络编程与JSON解析

24 | HTTP网络编程与JSON解析-极客时间

24 | HTTP网络编程与JSON解析

讲述:陈航

时长14:28大小13.24M

你好,我是陈航。
在上一篇文章中,我带你一起学习了 Dart 中异步与并发的机制及实现原理。与其他语言类似,Dart 的异步是通过事件循环与队列实现的,我们可以使用 Future 来封装异步任务。而另一方面,尽管 Dart 是基于单线程模型的,但也提供了 Isolate 这样的“多线程”能力,这使得我们可以充分利用系统资源,在并发 Isolate 中搞定 CPU 密集型的任务,并通过消息机制通知主 Isolate 运行结果。
异步与并发的一个典型应用场景,就是网络编程。一个好的移动应用,不仅需要有良好的界面和易用的交互体验,也需要具备和外界进行信息交互的能力。而通过网络,信息隔离的客户端与服务端间可以建立一个双向的通信通道,从而实现资源访问、接口数据请求和提交、上传下载文件等操作。
为了便于我们快速实现基于网络通道的信息交换实时更新 App 数据,Flutter 也提供了一系列的网络编程类库和工具。因此在今天的分享中,我会通过一些小例子与你讲述在 Flutter 应用中,如何实现与服务端的数据交互,以及如何将交互响应的数据格式化。

Http 网络编程

我们在通过网络与服务端数据交互时,不可避免地需要用到三个概念:定位、传输与应用。
其中,定位,定义了如何准确地找到网络上的一台或者多台主机(即 IP 地址);传输,则主要负责在找到主机后如何高效且可靠地进行数据通信(即 TCP、UDP 协议);而应用,则负责识别双方通信的内容(即 HTTP 协议)。
我们在进行数据通信时,可以只使用传输层协议。但传输层传递的数据是二进制流,如果没有应用层,我们无法识别数据内容。如果想要使传输的数据有意义,则必须要用到应用层协议。移动应用通常使用 HTTP 协议作应用层协议,来封装 HTTP 信息。
在编程框架中,一次 HTTP 网络调用通常可以拆解为以下步骤:
创建网络调用实例 client,设置通用请求行为(如超时时间);
构造 URI,设置请求 header、body;
发起请求, 等待响应;
解码响应的内容。
当然,Flutter 也不例外。在 Flutter 中,Http 网络编程的实现方式主要分为三种:dart:io 里的 HttpClient 实现、Dart 原生 http 请求库实现、第三方库 dio 实现。接下来,我依次为你讲解这三种方式。

HttpClient

HttpClient 是 dart:io 库中提供的网络请求类,实现了基本的网络编程功能。
接下来,我将和你分享一个实例,对照着上面提到的网络调用步骤,来演示 HttpClient 如何使用。
在下面的代码中,我们创建了一个 HttpClien 网络调用实例,设置了其超时时间为 5 秒。随后构造了 Flutter 官网的 URI,并设置了请求 Header 的 user-agent 为 Custom-UA。然后发起请求,等待 Flutter 官网响应。最后在收到响应后,打印出返回结果:
get() async {
//创建网络调用示例,设置通用请求行为(超时时间)
var httpClient = HttpClient();
httpClient.idleTimeout = Duration(seconds: 5);
//构造URI,设置user-agent为"Custom-UA"
var uri = Uri.parse("https://flutter.dev");
var request = await httpClient.getUrl(uri);
request.headers.add("user-agent", "Custom-UA");
//发起请求,等待响应
var response = await request.close();
//收到响应,打印结果
if (response.statusCode == HttpStatus.ok) {
print(await response.transform(utf8.decoder).join());
} else {
print('Error: \nHttp status ${response.statusCode}');
}
}
可以看到,使用 HttpClient 来发起网络调用还是相对比较简单的。
这里需要注意的是,由于网络请求是异步行为,因此在 Flutter 中,所有网络编程框架都是以 Future 作为异步请求的包装,所以我们需要使用 await 与 async 进行非阻塞的等待。当然,你也可以注册 then,以回调的方式进行相应的事件处理。

http

HttpClient 使用方式虽然简单,但其接口却暴露了不少内部实现细节。比如,异步调用拆分得过细,链接需要调用方主动关闭,请求结果是字符串但却需要手动解码等。
http 是 Dart 官方提供的另一个网络请求类,相比于 HttpClient,易用性提升了不少。同样,我们以一个例子来介绍 http 的使用方法。
首先,我们需要将 http 加入到 pubspec 中的依赖里:
dependencies:
http: '>=0.11.3+12'
在下面的代码中,与 HttpClient 的例子类似的,我们也是先后构造了 http 网络调用实例和 Flutter 官网 URI,在设置 user-agent 为 Custom-UA 后,发出请求,最后打印请求结果:
httpGet() async {
//创建网络调用示例
var client = http.Client();
//构造URI
var uri = Uri.parse("https://flutter.dev");
//设置user-agent为"Custom-UA",随后立即发出请求
http.Response response = await client.get(uri, headers : {"user-agent" : "Custom-UA"});
//打印请求结果
if(response.statusCode == HttpStatus.ok) {
print(response.body);
} else {
print("Error: ${response.statusCode}");
}
}
可以看到,相比于 HttpClient,http 的使用方式更加简单,仅需一次异步调用就可以实现基本的网络通信。

dio

HttpClient 和 http 使用方式虽然简单,但其暴露的定制化能力都相对较弱,很多常用的功能都不支持(或者实现异常繁琐),比如取消请求、定制拦截器、Cookie 管理等。因此对于复杂的网络请求行为,我推荐使用目前在 Dart 社区人气较高的第三方 dio 来发起网络请求。
接下来,我通过几个例子来和你介绍 dio 的使用方法。与 http 类似的,我们首先需要把 dio 加到 pubspec 中的依赖里:
dependencies:
dio: '>2.1.3'
在下面的代码中,与前面 HttpClient 与 http 例子类似的,我们也是先后创建了 dio 网络调用实例、创建 URI、设置 Header、发出请求,最后等待请求结果:
void getRequest() async {
//创建网络调用示例
Dio dio = new Dio();
//设置URI及请求user-agent后发起请求
var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
//打印请求结果
if(response.statusCode == HttpStatus.ok) {
print(response.data.toString());
} else {
print("Error: ${response.statusCode}");
}
}
这里需要注意的是,创建 URI、设置 Header 及发出请求的行为,都是通过 dio.get 方法实现的。这个方法的 options 参数提供了精细化控制网络请求的能力,可以支持设置 Header、超时时间、Cookie、请求方法等。这部分内容不是今天分享的重点,如果你想深入理解的话,可以访问其API 文档学习具体使用方法。
对于常见的上传及下载文件需求,dio 也提供了良好的支持:文件上传可以通过构建表单 FormData 实现,而文件下载则可以使用 download 方法搞定。
在下面的代码中,我们通过 FormData 创建了两个待上传的文件,通过 post 方法发送至服务端。download 的使用方法则更为简单,我们直接在请求参数中,把待下载的文件地址和本地文件名提供给 dio 即可。如果我们需要感知下载进度,可以增加 onReceiveProgress 回调函数:
//使用FormData表单构建待上传文件
FormData formData = FormData.from({
"file1": UploadFileInfo(File("./file1.txt"), "file1.txt"),
"file2": UploadFileInfo(File("./file2.txt"), "file1.txt"),
});
//通过post方法发送至服务端
var responseY = await dio.post("https://xxx.com/upload", data: formData);
print(responseY.toString());
//使用download方法下载文件
dio.download("https://xxx.com/file1", "xx1.zip");
//增加下载进度回调函数
dio.download("https://xxx.com/file1", "xx2.zip", onReceiveProgress: (count, total) {
//do something
});
有时,我们的页面由多个并行的请求响应结果构成,这就需要等待这些请求都返回后才能刷新界面。在 dio 中,我们可以结合 Future.wait 方法轻松实现:
//同时发起两个并行请求
List<Response> responseX= await Future.wait([dio.get("https://flutter.dev"),dio.get("https://pub.dev/packages/dio")]);
//打印请求1响应结果
print("Response1: ${responseX[0].toString()}");
//打印请求2响应结果
print("Response2: ${responseX[1].toString()}");
此外,与 Android 的 okHttp 一样,dio 还提供了请求拦截器,通过拦截器,我们可以在请求之前,或响应之后做一些特殊的操作。比如可以为请求 option 统一增加一个 header,或是返回缓存数据,或是增加本地校验处理等等。
在下面的例子中,我们为 dio 增加了一个拦截器。在请求发送之前,不仅为每个请求头都加上了自定义的 user-agent,还实现了基本的 token 认证信息检查功能。而对于本地已经缓存了请求 uri 资源的场景,我们可以直接返回缓存数据,避免再次下载:
//增加拦截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (RequestOptions options){
//为每个请求头都增加user-agent
options.headers["user-agent"] = "Custom-UA";
//检查是否有token,没有则直接报错
if(options.headers['token'] == null) {
return dio.reject("Error:请先登录");
}
//检查缓存是否有数据
if(options.uri == Uri.parse('http://xxx.com/file1')) {
return dio.resolve("返回缓存数据");
}
//放行请求
return options;
}
));
//增加try catch,防止请求报错
try {
var response = await dio.get("https://xxx.com/xxx.zip");
print(response.data.toString());
}catch(e) {
print(e);
}
需要注意的是,由于网络通信期间有可能会出现异常(比如,域名无法解析、超时等),因此我们需要使用 try-catch 来捕获这些未知错误,防止程序出现异常。
除了这些基本的用法,dio 还支持请求取消、设置代理,证书校验等功能。不过,这些高级特性不属于本次分享的重点,故不再赘述,详情可以参考 dio 的GitHub 主页了解具体用法。

JSON 解析

移动应用与 Web 服务器建立好了连接之后,接下来的两个重要工作分别是:服务器如何结构化地去描述返回的通信信息,以及移动应用如何解析这些格式化的信息。

如何结构化地描述返回的通信信息?

在如何结构化地去表达信息上,我们需要用到 JSON。JSON 是一种轻量级的、用于表达由属性值和字面量组成对象的数据交换语言。
一个简单的表示学生成绩的 JSON 结构,如下所示:
String jsonString = '''
{
"id":"123",
"name":"张三",
"score" : 95
}
''';
需要注意的是,由于 Flutter 不支持运行时反射,因此并没有提供像 Gson、Mantle 这样自动解析 JSON 的库来降低解析成本。在 Flutter 中,JSON 解析完全是手动的,开发者要做的事情多了一些,但使用起来倒也相对灵活。
接下来,我们就看看 Flutter 应用是如何解析这些格式化的信息。

如何解析格式化的信息?

所谓手动解析,是指使用 dart:convert 库中内置的 JSON 解码器,将 JSON 字符串解析成自定义对象的过程。使用这种方式,我们需要先将 JSON 字符串传递给 JSON.decode 方法解析成一个 Map,然后把这个 Map 传给自定义的类,进行相关属性的赋值。
以上面表示学生成绩的 JSON 结构为例,我来和你演示手动解析的使用方法。
首先,我们根据 JSON 结构定义 Student 类,并创建一个工厂类,来处理 Student 类属性成员与 JSON 字典对象的值之间的映射关系:
class Student{
//属性id,名字与成绩
String id;
String name;
int score;
//构造方法
Student({
this.id,
this.name,
this.score
});
//JSON解析工厂类,使用字典数据为对象初始化赋值
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
id: parsedJson['id'],
name : parsedJson['name'],
score : parsedJson ['score']
);
}
}
数据解析类创建好了,剩下的事情就相对简单了,我们只需要把 JSON 文本通过 JSON.decode 方法转换成 Map,然后把它交给 Student 的工厂类 fromJson 方法,即可完成 Student 对象的解析:
loadStudent() {
//jsonString为JSON文本
final jsonResponse = json.decode(jsonString);
Student student = Student.fromJson(jsonResponse);
print(student.name);
}
在上面的例子中,JSON 文本所有的属性都是基本类型,因此我们直接从 JSON 字典取出相应的元素为对象赋值即可。而如果 JSON 下面还有嵌套对象属性,比如下面的例子中,Student 还有一个 teacher 的属性,我们又该如何解析呢?
String jsonString = '''
{
"id":"123",
"name":"张三",
"score" : 95,
"teacher": {
"name": "李四",
"age" : 40
}
}
''';
这里,teacher 不再是一个基本类型,而是一个对象。面对这种情况,我们需要为每一个非基本类型属性创建一个解析类。与 Student 类似,我们也需要为它的属性 teacher 创建一个解析类 Teacher:
class Teacher {
//Teacher的名字与年龄
String name;
int age;
//构造方法
Teacher({this.name,this.age});
//JSON解析工厂类,使用字典数据为对象初始化赋值
factory Teacher.fromJson(Map<String, dynamic> parsedJson){
return Teacher(
name : parsedJson['name'],
age : parsedJson ['age']
);
}
}
然后,我们只需要在 Student 类中,增加 teacher 属性及对应的 JSON 映射规则即可:
class Student{
...
//增加teacher属性
Teacher teacher;
//构造函数增加teacher
Student({
...
this.teacher
});
factory Student.fromJson(Map<String, dynamic> parsedJson){
return Student(
...
//增加映射规则
teacher: Teacher.fromJson(parsedJson ['teacher'])
);
}
}
完成了 teacher 属性的映射规则添加之后,我们就可以继续使用 Student 来解析上述的 JSON 文本了:
final jsonResponse = json.decode(jsonString);//将字符串解码成Map对象
Student student = Student.fromJson(jsonResponse);//手动解析
print(student.teacher.name);
可以看到,通过这种方法,无论对象有多复杂的非基本类型属性,我们都可以创建对应的解析类进行处理。
不过到现在为止,我们的 JSON 数据解析还是在主 Isolate 中完成。如果 JSON 的数据格式比较复杂,数据量又大,这种解析方式可能会造成短期 UI 无法响应。对于这类 CPU 密集型的操作,我们可以使用上一篇文章中提到的 compute 函数,将解析工作放到新的 Isolate 中完成:
static Student parseStudent(String content) {
final jsonResponse = json.decode(content);
Student student = Student.fromJson(jsonResponse);
return student;
}
doSth() {
...
//用compute函数将json解析放到新Isolate
compute(parseStudent,jsonString).then((student)=>print(student.teacher.name));
}
通过 compute 的改造,我们就不用担心 JSON 解析时间过长阻塞 UI 响应了。

总结

好了,今天的分享就到这里了,我们简单回顾一下主要内容。
首先,我带你学习了实现 Flutter 应用与服务端通信的三种方式,即 HttpClient、http 与 dio。其中 dio 提供的功能更为强大,可以支持请求拦截、文件上传下载、请求合并等高级能力。因此,我推荐你在实际项目中使用 dio 的方式。
然后,我和你分享了 JSON 解析的相关内容。JSON 解析在 Flutter 中相对比较简单,但由于不支持反射,所以我们只能手动解析,即:先将 JSON 字符串转换成 Map,然后再把这个 Map 给到自定义类,进行相关属性的赋值。
如果你有原生 Android、iOS 开发经验的话,可能会觉得 Flutter 提供的 JSON 手动解析方案并不好用。在 Flutter 中,没有像原生开发那样提供了 Gson 或 Mantle 等库,用于将 JSON 字符串直接转换为对应的实体类。而这些能力无一例外都需要用到运行时反射,这是 Flutter 从设计之初就不支持的,理由如下:
运行时反射破坏了类的封装性和安全性,会带来安全风险。就在前段时间,Fastjson 框架就爆出了一个巨大的安全漏洞。这个漏洞使得精心构造的字符串文本,可以在反序列化时让服务器执行任意代码,直接导致业务机器被远程控制、内网渗透、窃取敏感信息等操作。
运行时反射会增加二进制文件大小。因为搞不清楚哪些代码可能会在运行时用到,因此使用反射后,会默认使用所有代码构建应用程序,这就导致编译器无法优化编译期间未使用的代码,应用安装包体积无法进一步压缩,这对于自带 Dart 虚拟机的 Flutter 应用程序是难以接受的。
反射给开发者编程带来了方便,但也带来了很多难以解决的新问题,因此 Flutter 并不支持反射。而我们要做的就是,老老实实地手动解析 JSON 吧。
我把今天分享所涉及到的知识点打包到了GitHub中,你可以下载下来,反复运行几次,加深理解与记忆。

思考题

最后,我给你留两道思考题吧。
请使用 dio 实现一个自定义拦截器,拦截器内检查 header 中的 token:如果没有 token,需要暂停本次请求,同时访问"http://xxxx.com/token",在获取新 token 后继续本次请求。
为以下 Student JSON 写相应的解析类:
String jsonString = '''
{
"id":"123",
"name":"张三",
"score" : 95,
"teachers": [
{
"name": "李四",
"age" : 40
},
{
"name": "王五",
"age" : 45
}
]
}
''';
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得18
生成海报并分享

赞 2

提建议

上一篇
23 | 单线程模型怎么保证UI运行流畅?
下一篇
25 | 本地存储与数据库的使用和优化
 写留言

精选留言(26)

  • 和小胖
    2019-10-09
    第一个问题解决方法: dio.interceptors.add(InterceptorsWrapper( onRequest: (Options options) async { if (options.headers['token'] == null) { print("no token,request token firstly..."); //lock the dio. dio.lock(); return new Dio().get("http://xxxx.com/token").then((d) { options.headers["token"] = d.data['token']; print("request token succeed, value: " + d.data['token']); print( 'continue to perform request:path:${options.path},baseURL:${options.path}'); return options; }).whenComplete(() => dio.unlock()); // unlock the dio } return options; } ));
    展开

    作者回复: 厉害👍

    共 2 条评论
    15
  • 江宁彭于晏
    2019-09-11
    分享一个json转dart类的工具,理解了原理后,实际项目中可以省不少时间https://javiercbk.github.io/json_to_dart/

    作者回复: 赞👍

    10
  • 给我点阳光就灿烂
    2019-08-22
    如何进行socket通信

    作者回复: 可以参考cnDart社区的文章:http://www.cndartlang.com/841.html

    共 2 条评论
    4
  • 和小胖
    2019-10-09
    第二道题解决方法: class Student { String id; String name; int score; List<Teacher> teachers; Student({this.id, this.name, this.score, this.teachers}); factory Student.fromJson(Map<String, dynamic> parsedJson) { return Student( id: parsedJson['id'], name: parsedJson['name'], score: parsedJson['score'], teachers: getTeacher(parsedJson['teachers'])); } static List<Teacher> getTeacher(dynamic list) { List<Teacher> teachers = new List(); list.forEach((f) { teachers.add(Teacher.fromJson(f)); }); return teachers; } } class Teacher { String name; int age; Teacher({this.age, this.name}); factory Teacher.fromJson(Map<String, dynamic> parsedJson) { return Teacher(name: parsedJson['name'], age: parsedJson['age']); } }
    展开

    作者回复: 👍厉害

    共 2 条评论
    4
  • Geek_0793f1
    2019-09-02
    使用这种方式,我们需要先将 JSON 字符串传递给 JSON.decode 方法解析成一个 Map,然后把这个 Map 传给自定义的类,进行相关属性的赋值。 前端一般把json字符串解析成map之后,就直接用这个map进行相关的属性赋值了,老师能解释一下,传给自定义类的做法的好处吗?

    作者回复: 以对象的方式承接服务端返回的JSON字典,不仅更直观,也避免了一些因为key写错出现的代码错误。js的类型系统比较混乱,object和字典是可以混用的,比如下面这段代码: var x = {'number':123,'title':'title'}; x.number和x['number']是等价的,所以这也就是为什么前端同学直接拿字典就可以当对象用了。 而Dart是强类型语言,字典里的键值对和对象的属性是不能混用的,所以我们需要定义一层映射关系。

    共 2 条评论
    2
  • 飓风
    2020-07-21
    https://github.com/trevorwang/retrofit.dart/ 这个库配合dio,网络请求转换这一步我觉得更简单点?
    1
  • Geek_0d3a08
    2019-08-29
    重定向监听有吗?

    作者回复: Dio默认会自动帮你重定向,你可以在options参数里面把followRedirects置为false,自己在拦截器中处理重定向

    1
  • 江厚宏
    2019-08-22
    老师能不能介绍一下反序列化工具,比如json_serializable和 built_value,建议用哪一个,如果遇到泛型,该如何处理

    作者回复: 其实我觉得Dart提供的JSON自动序列化方案并不好用,所以我还是推荐手动解析,对于泛型,直接在运行是判断类型,走对应的解析方法就可以了。 看你的使用场景,一般而言,json_serializable易用性上比built_value会更好一些。

    1
  • 李鑫鑫
    2022-02-26
    带有泛型 的bean 这么转json????
  • 小何
    2021-07-30
    老师好,问一个问题,flutter怎么做回调啊,我想封装一个工具类,网络请求的时候传入callback,然后成功就调用callback.onSuccess(t),失败就调用callback.onFailed(t)
  • 小何
    2021-07-21
    老师好,compute和Future区别是什么啊, 不是用Future也可以实现异步嘛
    共 1 条评论
  • 任逍遥
    2021-07-12
    最近遇到一个问题,问一下老师服务器更新SSL证书时使用了pem格式,发现安卓端请求失败,iOS端正常,flutter应该如何兼容pem证书呢
  • IF-Processing
    2020-02-07
    请问,flutter对于网络服务(比如Samba,NFS共享)的访问有支持吗?
  • 安南寸暖🤕
    2019-11-07
    json_model 怎么生成纯数组的解析代码?

    作者回复: 参考官方文档吧https://github.com/flutterchina/json_model

  • 大神博士
    2019-09-21
    想问下 Flutter 中 JSONP 的请求怎么处理

    作者回复: 1.直接发url请求就可以了、把服务端返回的数据当作字符串简单处理下,把它转成json格式 2.flutter不需要处理跨域问题,不建议用jsonp这种奇怪的格式

    共 2 条评论
  • 米米呀👧
    2019-09-10
    import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; [...] loadData() async { String dataURL = "https://jsonplaceholder.typicode.com/posts"; http.Response response = await http.get(dataURL); setState(() { widgets = JSON.decode(response.body); }); } } 官网Demo里面是用的这个,跟HttpClient有什么区别?我该用哪个?
    展开

    作者回复: 这个就是今天分享说的http包啊

  • Geek_joestar
    2019-09-08
    static List<Teacher> fromJsonList(List<dynamic> listJson){ var list = List<Teacher>(); for(Map<String, dynamic> parsedJson in listJson) { list.add(Teacher.fromJson(parsedJson)); } return list; }
    展开

    作者回复: 赞

  • C
    2019-09-06
    class Teacher { String name; int age; Teacher({this.name, this.age}); factory Teacher.fromJson(Map<String, dynamic> parsedJson) { return Teacher( name: parsedJson['name'], age: parsedJson['age'], ); } static List<Teacher> parseTeachers(List<dynamic> mapList) { List<Teacher> teachers = List<Teacher>(); for(Map<String, dynamic> map in mapList) { teachers.add(Teacher.fromJson(map)); } return teachers; } } class Student { String id; String name; int score; List<Teacher> teachers; Student({this.id, this.name, this.score, this.teachers}); factory Student.fromJson(Map<String, dynamic> parsedJson) { return Student( id: parsedJson['id'], name: parsedJson['name'], score: parsedJson['score'], teachers: Teacher.parseTeachers(parsedJson['teachers']), ); } }
    展开

    作者回复: 很赞👍

  • 大和
    2019-09-04
    dio.interceptors.add(InterceptorsWrapper( onRequest: (RequestOptions options) async { if (options.headers["token"] == null) { try { var token = await new Dio().get("https://xxx.com/token"); options.headers["token"] = token; }catch(e) { return dio.reject("Error: 请先登录..."); } } return options; } ));
    展开

    作者回复: 思路是对的,有一个小点可以改进:如果此刻有多个request同时发出,token会请求多次。

  • 宁缺
    2019-09-03
    请问老大,课后作业的答案啥时候给参考一下

    作者回复: 会考虑专门出一节讲课后题的解决思路