Http 网络编程

我们在通过网络与服务端数据交互时,不可避免地需要用到三个概念:定位、传输与应用。

  • 定位,定义了如何准确地找到网络上的一台或者多台主机(即 IP 地址);
  • 传输,则主要负责在找到主机后如何高效且可靠地进行数据通信(即 TCP、UDP 协议);
  • 应用,则负责识别双方通信的内容(即 HTTP 协议)。

在编程框架中,一次 HTTP 网络调用通常可以拆解为以下步骤:

  1. 创建网络调用实例 client,设置通用请求行为(如超时时间);
  2. 构造 URI,设置请求 header、body;
  3. 发起请求, 等待响应;
  4. 解码响应的内容。

在 Flutter 中,Http 网络编程的实现方式主要分为三种:dart:io 里的 HttpClient 实现、Dart 原生 http 请求库实现、第三方库 dio 实现。

HttpClient

HttpClient 是 dart:io 库中提供的网络请求类,实现了基本的网络编程功能。

在下面的代码中,我们创建了一个 HttpClien 网络调用实例,设置了其超时时间为 5 秒。随后构造了 Flutter 官网的 URI,并设置了请求 Header 的 user-agent 为 Custom-UA。然后发起请求,等待 Flutter 官网响应。最后在收到响应后,打印出返回结果:

  1. get() async {
  2. //创建网络调用示例,设置通用请求行为(超时时间)
  3. var httpClient = HttpClient();
  4. httpClient.idleTimeout = Duration(seconds: 5);
  5. //构造URI,设置user-agent为"Custom-UA"
  6. var uri = Uri.parse("https://flutter.dev");
  7. var request = await httpClient.getUrl(uri);
  8. request.headers.add("user-agent", "Custom-UA");
  9. //发起请求,等待响应
  10. var response = await request.close();
  11. //收到响应,打印结果
  12. if (response.statusCode == HttpStatus.ok) {
  13. print(await response.transform(utf8.decoder).join());
  14. } else {
  15. print('Error: \nHttp status ${response.statusCode}');
  16. }
  17. }

由于网络请求是异步行为,因此在 Flutter 中,所有网络编程框架都是以 Future 作为异步请求的包装,所以我们需要使用 await 与 async 进行非阻塞的等待。当然,你也可以注册 then,以回调的方式进行相应的事件处理。

http

HttpClient 使用方式虽然简单,但其接口却暴露了不少内部实现细节。比如,异步调用拆分得过细,链接需要调用方主动关闭,请求结果是字符串但却需要手动解码等。

http 是 Dart 官方提供的另一个网络请求类,相比于 HttpClient,易用性提升了不少。同样,我们以一个例子来介绍 http 的使用方法。

  1. 首先,我们需要将 http 加入到 pubspec 中的依赖里:
  1. dependencies:
  2. http: '>=0.11.3+12'
  1. 在下面的代码中,与 HttpClient 的例子类似的,我们也是先后构造了 http 网络调用实例和 Flutter 官网 URI,在设置 user-agent 为 Custom-UA 后,发出请求,最后打印请求结果:
  1. httpGet() async {
  2. //创建网络调用示例
  3. var client = http.Client();
  4. //构造URI
  5. var uri = Uri.parse("https://flutter.dev");
  6. //设置user-agent为"Custom-UA",随后立即发出请求
  7. http.Response response = await client.get(uri, headers : {"user-agent" : "Custom-UA"});
  8. //打印请求结果
  9. if(response.statusCode == HttpStatus.ok) {
  10. print(response.body);
  11. } else {
  12. print("Error: ${response.statusCode}");
  13. }
  14. }

dio

我推荐使用目前在 Dart 社区人气较高的第三方 dio 来发起网络请求。

  1. 首先需要把 dio 加到 pubspec 中的依赖里:
  1. dependencies:
  2. dio: '>2.1.3'
  1. 在下面的代码中,与前面 HttpClient 与 http 例子类似的,我们也是先后创建了 dio 网络调用实例、创建 URI、设置 Header、发出请求,最后等待请求结果:
  1. void getRequest() async {
  2. //创建网络调用示例
  3. Dio dio = new Dio();
  4. //设置URI及请求user-agent后发起请求
  5. var response = await dio.get("https://flutter.dev", options:Options(headers: {"user-agent" : "Custom-UA"}));
  6. //打印请求结果
  7. if(response.statusCode == HttpStatus.ok) {
  8. print(response.data.toString());
  9. } else {
  10. print("Error: ${response.statusCode}");
  11. }
  12. }

对于常见的上传及下载文件需求,dio 也提供了良好的支持:文件上传可以通过构建表单 FormData 实现,而文件下载则可以使用 download 方法搞定。

在下面的代码中,我们通过 FormData 创建了两个待上传的文件,通过 post 方法发送至服务端。download 的使用方法则更为简单,我们直接在请求参数中,把待下载的文件地址和本地文件名提供给 dio 即可。如果我们需要感知下载进度,可以增加 onReceiveProgress 回调函数:

  1. //使用FormData表单构建待上传文件
  2. FormData formData = FormData.from({
  3. "file1": UploadFileInfo(File("./file1.txt"), "file1.txt"),
  4. "file2": UploadFileInfo(File("./file2.txt"), "file1.txt"),
  5. });
  6. //通过post方法发送至服务端
  7. var responseY = await dio.post("https://xxx.com/upload", data: formData);
  8. print(responseY.toString());
  9. //使用download方法下载文件
  10. dio.download("https://xxx.com/file1", "xx1.zip");
  11. //增加下载进度回调函数
  12. dio.download("https://xxx.com/file1", "xx2.zip", onReceiveProgress: (count, total) {
  13. //do something
  14. });

有时,我们的页面由多个并行的请求响应结果构成,这就需要等待这些请求都返回后才能刷新界面。在 dio 中,我们可以结合 Future.wait 方法轻松实现:

  1. //同时发起两个并行请求
  2. List<Response> responseX= await Future.wait([dio.get("https://flutter.dev"),dio.get("https://pub.dev/packages/dio")]);
  3. //打印请求1响应结果
  4. print("Response1: ${responseX[0].toString()}");
  5. //打印请求2响应结果
  6. print("Response2: ${responseX[1].toString()}");

此外,与 Android 的 okHttp 一样,dio 还提供了请求拦截器,通过拦截器,我们可以在请求之前,或响应之后做一些特殊的操作。比如可以为请求 option 统一增加一个 header,或是返回缓存数据,或是增加本地校验处理等等。

  1. //增加拦截器
  2. dio.interceptors.add(InterceptorsWrapper(
  3. onRequest: (RequestOptions options){
  4. //为每个请求头都增加user-agent
  5. options.headers["user-agent"] = "Custom-UA";
  6. //检查是否有token,没有则直接报错
  7. if(options.headers['token'] == null) {
  8. return dio.reject("Error:请先登录");
  9. }
  10. //检查缓存是否有数据
  11. if(options.uri == Uri.parse('http://xxx.com/file1')) {
  12. return dio.resolve("返回缓存数据");
  13. }
  14. //放行请求
  15. return options;
  16. }
  17. ));
  18. //增加try catch,防止请求报错
  19. try {
  20. var response = await dio.get("https://xxx.com/xxx.zip");
  21. print(response.data.toString());
  22. }catch(e) {
  23. print(e);
  24. }

JSON 解析

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

一个简单的表示学生成绩的 JSON 结构,如下所示:

  1. String jsonString = '''
  2. {
  3. "id":"123",
  4. "name":"张三",
  5. "score" : 95
  6. }
  7. ''';

需要注意的是,由于 Flutter 不支持运行时反射,因此并没有提供像 Gson、Mantle 这样自动解析 JSON 的库来降低解析成本。在 Flutter 中,JSON 解析完全是手动的

如何解析格式化的信息?

所谓手动解析,是指使用 dart:convert 库中内置的 JSON 解码器,将 JSON 字符串解析成自定义对象的过程。使用这种方式,我们需要先将 JSON 字符串传递给 JSON.decode 方法解析成一个 Map,然后把这个 Map 传给自定义的类,进行相关属性的赋值。

  1. 首先,我们根据 JSON 结构定义 Student 类,并创建一个工厂类,来处理 Student 类属性成员与 JSON 字典对象的值之间的映射关系:
  1. class Student{
  2. //属性id,名字与成绩
  3. String id;
  4. String name;
  5. int score;
  6. //构造方法
  7. Student({
  8. this.id,
  9. this.name,
  10. this.score
  11. });
  12. //JSON解析工厂类,使用字典数据为对象初始化赋值
  13. factory Student.fromJson(Map<String, dynamic> parsedJson){
  14. return Student(
  15. id: parsedJson['id'],
  16. name : parsedJson['name'],
  17. score : parsedJson ['score']
  18. );
  19. }
  20. }
  1. 数据解析类创建好了,剩下的事情就相对简单了,我们只需要把 JSON 文本通过 JSON.decode 方法转换成 Map,然后把它交给 Student 的工厂类 fromJson 方法,即可完成 Student 对象的解析:
  1. loadStudent() {
  2. //jsonString为JSON文本
  3. final jsonResponse = json.decode(jsonString);
  4. Student student = Student.fromJson(jsonResponse);
  5. print(student.name);
  6. }

如果 JSON 下面还有嵌套对象属性,比如下面的例子中,Student 还有一个 teacher 的属性,我们又该如何解析呢?

  1. String jsonString = '''
  2. {
  3. "id":"123",
  4. "name":"张三",
  5. "score" : 95,
  6. "teacher": {
  7. "name": "李四",
  8. "age" : 40
  9. }
  10. }
  11. ''';

这里,teacher 不再是一个基本类型,而是一个对象。面对这种情况,我们需要为每一个非基本类型属性创建一个解析类。与 Student 类似,我们也需要为它的属性 teacher 创建一个解析类 Teacher:

  1. class Teacher {
  2. //Teacher的名字与年龄
  3. String name;
  4. int age;
  5. //构造方法
  6. Teacher({this.name,this.age});
  7. //JSON解析工厂类,使用字典数据为对象初始化赋值
  8. factory Teacher.fromJson(Map<String, dynamic> parsedJson){
  9. return Teacher(
  10. name : parsedJson['name'],
  11. age : parsedJson ['age']
  12. );
  13. }
  14. }

然后,我们只需要在 Student 类中,增加 teacher 属性及对应的 JSON 映射规则即可:

  1. class Student{
  2. ...
  3. //增加teacher属性
  4. Teacher teacher;
  5. //构造函数增加teacher
  6. Student({
  7. ...
  8. this.teacher
  9. });
  10. factory Student.fromJson(Map<String, dynamic> parsedJson){
  11. return Student(
  12. ...
  13. //增加映射规则
  14. teacher: Teacher.fromJson(parsedJson ['teacher'])
  15. );
  16. }
  17. }

完成了 teacher 属性的映射规则添加之后,我们就可以继续使用 Student 来解析上述的 JSON 文本了:

  1. final jsonResponse = json.decode(jsonString);//将字符串解码成Map对象
  2. Student student = Student.fromJson(jsonResponse);//手动解析
  3. print(student.teacher.name);

不过到现在为止,我们的 JSON 数据解析还是在主 Isolate 中完成。如果 JSON 的数据格式比较复杂,数据量又大,这种解析方式可能会造成短期 UI 无法响应。对于这类 CPU 密集型的操作,我们可以使用上一篇文章中提到的 compute 函数,将解析工作放到新的 Isolate 中完成:
**

  1. static Student parseStudent(String content) {
  2. final jsonResponse = json.decode(content);
  3. Student student = Student.fromJson(jsonResponse);
  4. return student;
  5. }
  6. doSth() {
  7. ...
  8. //用compute函数将json解析放到新Isolate
  9. compute(parseStudent,jsonString).then((student)=>print(student.teacher.name));
  10. }