乐趣区

Flutter开发系列教程之网络请求

简介

Http 网络请求是一门开发语言里比较常用和重要的功能,主要用于资源访问、接口数据请求和提交、上传下载文件等等操作,Http 请求方式主要有:GET、POST、HEAD、PUT、DELETE、TRACE、CONNECT、OPTIONS。本文主要 GET 和 POST 这两种常用请求在 Flutter 中的用法,其中对 POST 将进行着重讲解。Flutter 的 Http 网络请求的实现主要分为三种:io.dart 里的 HttpClient、Dart 原生 http 请求和第三方库实现。

Http 网络请求是互联网开发的基础协议,Http 支持的请求方式有:GET、POST、HEAD、PUT、DELETE、TRACE、CONNECT、OPTIONS 这八种。

GET 请求

GET 请求主要是执行获取资源操作的,例如通过 URL 从服务器获取返回的资源,其中 GET 可以把请求的一些参数信息拼接在 URL 上,传递给服务器,由服务器端进行参数信息解析,服务器收到请求后返回相应的资源给请求者。注意:GET 请求拼接的 URL 数据大小和长度是有最大限制的,传输的数据量一般限制在 2KB。

POST 请求

POST 请求主要用于执行提交信息、请求信息等操作,相比 GET 请求,POST 请求的可以携带更多的数据,而且格式不限,如 JSON、XML、文本等等都支持。并且 POST 传递的一些数据和参数不是直接拼接在 URL 后的,而是放在 Http 请求 Body 里,相对 GET 来说比较安全。并且传递的数据大小和格式是无限制的。
POST 请求方式是一种比较常用网络请求方式,通常由请求头(header)和请求体(body)两部分组成。POST 请求常见的请求体(body)有三种传输内容类型 Content-type:application/x-www-form-urlencoded、application/json 和 multipart/form-data,当然还有其他的几种,不过不常用,常用的就是这三种。

HEAD 请求

HEAD 请求主要用于给请求的客户端返回头信息,而不返回 Body 主体内容。和 GET 方式类似,只不过 GET 方式有 Body 实体返回,而 HEAD 只返回头信息,无 Body 实体内容返回。主要是用于确认 URL 的有效性、资源更新的日期时间、查看服务器状态等等,对于有这方面需求的请求来说,比较不占用资源。

PUT 请求

PUT 请求主要用于执行传输文件操作,类似于 FTP 的文件上传一样,请求里包含文件内容,并将此文件保存到 URI 指定的服务器位置。
和 POST 方式的主要区别是:PUT 请求方式如果前后两个请求相同,则后一个请求会把前一个请求覆盖掉,实现了 PUT 方式的修改资源;而 POST 请求方式如果前后两个请求相同,则后一个请求不会把前一个请求覆盖掉,实现了 POST 的增加资源。

DELETE 请求

DELETE 请求主要用于执行删除操作,告诉服务器想要删除的资源,不常用。

OPTIONS 请求

OPTIONS 请求主要用于执行查询针对所要请求的 URI 资源服务器所支持的请求方式,也就是获取这个 URI 所支持客户端提交给服务器端的请求方式有哪些。

TRACE 请求

TRACE 请求主要用于执行追踪传输路径的操作,例如,我们发起了一个 Http 请求,在这个过程中这个请求可能会经过很多个路径和过程,TRACE 就是告诉服务器在收到请求后,返回一条响应信息,将它收到的原始 Http 请求信息返回给客户端,这样就可以验证在 Http 传输过程中请求是否被修改过。

CONNECT 请求

CONNECT 请求主要用于执行连接代理操作,例如“翻墙”。客户端通过 CONNECT 方式与服务器建立通信隧道,进行 TCP 通信。主要通过 SSL 和 TLS 安全传输数据。CONNECT 的作用就是告诉服务器让它代替客户端去请求访问某个资源,然后再将数据返回给客户端,相当于一个媒介中转。

Dart 的 Http 请求

Dart 原生 http 请求库是 Dart 提供的一种请求方式,常见的请求方式都支持,除此之外,还支持如上传和下载文件等操作。

Dart 官方仓库提供了大量的三方库和官方库,引用也非常的方便,Dart PUB 官方地址为:https://pub.dartlang.org,如下图所示:

1.1 安装依赖

使用 Dart 的原生 http 库进行网络请求时,需要先在 Dart PUB 或官方 Github 里把相关的 http 库引用下来,然后才能使用。添加包依赖前,我们可以使用 https://pub.dev/packages/http…。

然后,在 pubspec.yaml 文件的 dependencies 节点添加 http 库依赖,如下所示:

http: ^0.12.0+2

然后,使用 flutter packages get 命令拉取库依赖。使用 http 进行网络请求前,需要先导入 http 包,如下:

import 'package:http/http.dart' as http;

1.2 常用方法

http 库支持常见的 get、post、del 等请求。其中,get 请求的格式如下:

get(dynamic url, { Map<String, String> headers}) → Future<Response>
  • (必须)url: 请求地址
  • (可选)headers: 请求头

post 请求的格式如下:

post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding}) → Future<Response>
  • (必须)url:请求地址
  • (可选)headers:请求头
  • (可选)body:参数
  • (编码)Encoding:编码

例如,下面是 post 的示例:

http.post('https://flutter-cn.firebaseio.com/products.json',
            body: json.encode(param),encoding: Utf8Codec())
    .then((http.Response response) {final Map<String, dynamic> responseData = json.decode(response.body);
       // 处理响应数据
    
    }).catchError((error) {print('$error 错误');
    });

1.3 示例

例如,下面使用 Dart 的 http 库实现 get 请求的示例,示例代码如下:

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(MyApp());

var hotMovies =
    'https://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a';

class MyApp extends StatelessWidget {
  var movies = '';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'http 请求示例',
        theme: new ThemeData(primaryColor: Colors.white,),
        home: new Scaffold(
          appBar: new AppBar(title: new Text('http 请求示例'),
          ),
          body: new Column(children: <Widget>[
            new RaisedButton(child: new Text('获取电影列表'), onPressed: getFilmList()),
            new Expanded(child: new Text('$movies'),
            )
          ]),
        ));
  }

  getFilmList() {http.get(hotMovies).then((response) {movies = response.body;});
  }
}

运行上面的代码,结果如下图:

除了 get 请求,http 的 post 请求示例如下:

import 'dart:convert';
import 'dart:io';

import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';

class DartHttpUtils {
  // 创建 client 实例
  var _client = http.Client();

  // 发送 GET 请求
  getClient() async {
    var url = "https://abc.com:8090/path1?name=abc&pwd=123";
    _client.get(url).then((http.Response response) {
      // 处理响应信息
      if (response.statusCode == 200) {print(response.body);
      } else {print('error');
      }
    });
  }

// 发送 POST 请求,application/x-www-form-urlencoded
  postUrlencodedClient() async {
    var url = "https://abc.com:8090/path2";
    // 设置 header
    Map<String, String> headersMap = new Map();
    headersMap["content-type"] = "application/x-www-form-urlencoded";
    // 设置 body 参数
    Map<String, String> bodyParams = new Map();
    bodyParams["name"] = "value1";
    bodyParams["pwd"] = "value2";
    _client
        .post(url, headers: headersMap, body: bodyParams, encoding: Utf8Codec())
        .then((http.Response response) {if (response.statusCode == 200) {print(response.body);
      } else {print('error');
      }
    }).catchError((error) {print('error');
    });
  }

  // 发送 POST 请求,application/json
  postJsonClient() async {
    var url = "https://abc.com:8090/path3";
    Map<String, String> headersMap = new Map();
    headersMap["content-type"] = ContentType.json.toString();
    Map<String, String> bodyParams = new Map();
    bodyParams["name"] = "value1";
    bodyParams["pwd"] = "value2";
    _client
        .post(url,
            headers: headersMap,
            body: jsonEncode(bodyParams),
            encoding: Utf8Codec())
        .then((http.Response response) {if (response.statusCode == 200) {print(response.body);
      } else {print('error');
      }
    }).catchError((error) {print('error');
    });
  }

  // 发送 POST 请求,multipart/form-data
  postFormDataClient() async {
    var url = "https://abc.com:8090/path4";
    var client = new http.MultipartRequest("post", Uri.parse(url));
    client.fields["name"] = "value1";
    client.fields["pwd"] = "value2";
    client.send().then((http.StreamedResponse response) {if (response.statusCode == 200) {response.stream.transform(utf8.decoder).join().then((String string) {print(string);
        });
      } else {print('error');
      }
    }).catchError((error) {print('error');
    });
  }

// 发送 POST 请求,multipart/form-data,上传文件
  postFileClient() async {
    var url = "https://abc.com:8090/path5";
    var client = new http.MultipartRequest("post", Uri.parse(url));
    http.MultipartFile.fromPath('file', 'sdcard/img.png',
            filename: 'img.png', contentType: MediaType('image', 'png'))
        .then((http.MultipartFile file) {client.files.add(file);
      client.fields["description"] = "descriptiondescription";
      client.send().then((http.StreamedResponse response) {if (response.statusCode == 200) {response.stream.transform(utf8.decoder).join().then((String string) {print(string);
          });
        } else {response.stream.transform(utf8.decoder).join().then((String string) {print(string);
          });
        }
      }).catchError((error) {print(error);
      });
    });
  }
  /// 其余的 HEAD、PUT、DELETE 请求用法类似,大同小异,大家可以自己试一下
  /// 在 Widget 里请求成功数据后,使用 setState 来更新内容和状态即可
  ///setState(() {
  ///    ...
  ///  });
}

HttpClient 请求

Dart IO 库中提供的 HttpClient 可以实现一些基本的 Http 请求。不过,HttpClient 只能实现一些基本的网络请求,对应一些复杂的网络请求还无法完成,如 POST 里的 Body 请求体传输内容类型部分还无法支持,multipart/form-data 这个类型传输还不支持。

2.1 使用方法

使用 HttpClient 发起请求主要分为五步:
1,创建一个 HttpClient。

HttpClient httpClient = new HttpClient();

2,打开 Http 连接,设置请求头。

HttpClientRequest request = await httpClient.getUrl(uri);

在这一步,我们可以使用任意 Http method,如 httpClient.post(…)、httpClient.delete(…) 等。如果包含 Query 参数,可以在构建 uri 时添加,如:

Uri uri=Uri(scheme: "https", host: "flutterchina.club", queryParameters: {
    "xx":"xx",
    "yy":"dd"
  });

如果需要设置请求头,可以通过 HttpClientRequest 设置请求 header,如:

request.headers.add("user-agent", "test");

如果是 post 或 put 等可以携带请求体的请求,还可以通过 HttpClientRequest 对象发送 request body,如:

String payload="...";
request.add(utf8.encode(payload)); 
//request.addStream(_inputStream); // 可以直接添加输入流 

3,等待连接服务器。

HttpClientResponse response = await request.close();

到这一步之后,请求信息就已经发送给服务器了,返回一个 HttpClientResponse 对象,它包含响应头(header)和响应流 (响应体的 Stream),接下来就可以通过读取响应流来获取响应内容。

4,读取响应内容

String responseBody = await response.transform(utf8.decoder).join();

5,请求结束后,还需要关闭 HttpClient。

httpClient.close();

2.2 请求示例

import 'package:flutter/material.dart';
import 'dart:convert';
import 'dart:io';

void main() => runApp(MyApp());

var hotMovies =
    'https://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a';

class MyApp extends StatelessWidget {

  var movies = '';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'HttpClient 请求示例',
        theme: new ThemeData(primaryColor: Colors.white,),
        home: new Scaffold(
          appBar: new AppBar(title: new Text('HttpClient 请求示例'),
          ),
          body: new Column(children: <Widget>[
            new RaisedButton(child: new Text('获取电影列表'), onPressed: getFilmList),
            new Expanded(child: new Text('$movies'),
            )
          ]),
        ));
  }

 void getFilmList() async {
    try {HttpClient httpClient = new HttpClient();
      HttpClientRequest request = await httpClient.getUrl(Uri.parse(hotMovies));
      HttpClientResponse response = await request.close();
      var result = await response.transform(utf8.decoder).join();
      movies = result;
      print('movies'+result);
      httpClient.close();}catch(e){print('请求失败:$e');
    }
  }
}

执行上面的代码,结果如下图:

dio 库

除了上面两种常见的请求方式外,Flutter 开发中还可以使用 dio 等第三方库来实现 Http 网络请求,如 Dart 社区提供的 dio 库。

前面说过,HttpClient 发起网络请求是比较麻烦的,很多事情都需要我们手动处理,如果再涉及到文件上传 / 下载、Cookie 管理等就会非常繁琐。而 Dart 社区有一些第三方 http 请求库,就可以简化这些操作。dio 库不仅支持常见的网络请求,还支持 Restful API、FormData、拦截器、请求取消、Cookie 管理、文件上传 / 下载、超时等操作。

3.1 安装依赖

和使用其他的第三方库一样,使用 dio 库之前需要先安装依赖,安装前可以在 Dart PUB 上搜索 dio,确定其版本号,如下所示:

dependencies:
  dio: 2.1.x  #latest version

然后,执行 flutter packages get 命令或者点击【Packages get】选项拉取库依赖。
使用 dio 之前需要先导入 dio 库,并创建 dio 实例,如下所示:

import 'package:dio/dio.dart';
Dio dio = new Dio();

接下来,就可以通过 dio 实例来发起网络请求了,注意,一个 dio 实例可以发起多个 http 请求,一般来说,APP 只有一个 http 数据源时,dio 应该使用单例模式。

3.2 使用方法

3.2.1 GET 请求

import 'package:dio/dio.dart';
void getHttp() async {
  try {
    Response response;
   response=await dio.get("/test?id=12&name=wendu")
   print(response.data.toString());
  } catch (e) {print(e);
  }
}

在上面的示例中,我们可以将 query 参数通过对象来传递,上面的代码等同于:

response=await dio.get("/test",queryParameters:{"id":12,"name":"wendu"})
print(response);

3.2.2 POST 请求

response=await dio.post("/test",data:{"id":12,"name":"wendu"})

3.2.3 多个并发请求

如果要发起多个并发请求,可以使用下面的方式:

response= await Future.wait([dio.post("/info"),dio.get("/token")]);

3.2.4 下载文件

如果要下载文件,可以使用 dio 的 download 函数,如下所示:

response=await dio.download("https://www.google.com/",_savePath);

3.2.5 FormData 请求

如果要发起表单请求,可以使用下面的方式:

FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
});
response = await dio.post("/info", data: formData)

如果发送的数据是 FormData,则 dio 会将请求 header 的 contentType 设为“multipart/form-data”。
当然,FormData 也支持上传多个文件操作,例如:

FormData formData = new FormData.from({
   "name": "wendux",
   "age": 25,
   "file1": new UploadFileInfo(new File("./upload.txt"), "upload1.txt"),
   "file2": new UploadFileInfo(new File("./upload.txt"), "upload2.txt"),
     // 支持文件数组上传
   "files": [new UploadFileInfo(new File("./example/upload.txt"), "upload.txt"),
      new UploadFileInfo(new File("./example/upload.txt"), "upload.txt")
    ]
});
response = await dio.post("/info", data: formData)

3.2.6 回调设置

值得一提的是,dio 内部仍然使用 HttpClient 发起的请求,所以代理、请求认证、证书校验等和 HttpClient 是相同的,我们可以在 onHttpClientCreate 回调中进行设置,例如:

(dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate = (client) {
    // 设置代理 
    client.findProxy = (uri) {return "PROXY 192.168.1.2:8888";};
    // 校验证书
    httpClient.badCertificateCallback=(X509Certificate cert, String host, int port){if(cert.pem==PEM){return true; // 证书一致,则允许发送数据}
     return false;
    };   
  };

3.3 示例

import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

void main() => runApp(MyApp());

var hotMovies = 'http://api.douban.com/v2/movie/in_theaters?apikey=0df993c66c0c636e29ecbb5344252a4a';

class MyApp extends StatelessWidget {
  var movies = '';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Dio 请求示例',
        theme: new ThemeData(primaryColor: Colors.white,),
        home: new Scaffold(
          appBar: new AppBar(title: new Text('Dio 请求示例'),
          ),
          body: new Column(children: <Widget>[
            new RaisedButton(child: new Text('获取电影列表'), onPressed: getFilmList),
            new Expanded(child: new Text('$movies'),
            )
          ]),
        ));
  }

  void getFilmList() async {Dio dio = new Dio();
    Response response=await dio.get(hotMovies);
    movies=response.toString();
    print('电影数据:'+movies);
  }
}

综合示例

为了对前面的知识做一个简单的总结,下面通过一个见得的示例来讲解 Flutter 的基本使用,最终效果如图:

需要说的是,最新版本豆瓣 api 需要传递 apikey 才能获取值,下面是电影列表的源码:

import 'package:flutter/material.dart';
import 'dart:convert' as Convert;
import 'dart:io';
import 'package:flutter/cupertino.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '豆瓣电影',
      home: Scaffold(
        appBar: new AppBar(title: new Text('豆瓣电影列表'),
        ),
        body: DouBanListView(),),
    );
  }
}


class DouBanListView extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {return DouBanState();
  }
}

class DouBanState extends State<DouBanListView> with AutomaticKeepAliveClientMixin{

  var url='http://api.douban.com/v2/movie/top250?start=25&count=10&apikey=0df993c66c0c636e29ecbb5344252a4a';
  var subjects = [];
  var itemHeight = 150.0;

  requestMovieTop() async {var httpClient = new HttpClient();
    var request = await httpClient.getUrl(Uri.parse(url));
    var response = await request.close();
    var responseBody = await response.transform(Convert.utf8.decoder).join();
    Map data = Convert.jsonDecode(responseBody);
    setState(() {subjects = data['subjects'];
    });
  }

  @override
  void initState() {super.initState();
    requestMovieTop();}

  @override
  Widget build(BuildContext context) {
    return Container(child: getListViewContainer(),
    );
  }

  getListViewContainer() {if (subjects.length == 0) {
      //loading
      return CupertinoActivityIndicator();}
    return ListView.builder(
      //item 的数量
        itemCount: subjects.length,
        itemBuilder: (BuildContext context, int index) {
          return GestureDetector(
            //Flutter 手势处理
            child: Container(
              color: Colors.transparent,
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: <Widget>[numberWidget(index + 1),
                  getItemContainerView(subjects[index]),
                  // 下面的灰色分割线
                  Container(
                    height: 10,
                    color: Color.fromARGB(255, 234, 233, 234),
                  )
                ],
              ),
            ),
            onTap: () {
              // 监听点击事件
              print("click item index=$index");
            },
          );
        });
  }

  // 肖申克的救赎 (1993) View
  getTitleView(subject) {var title = subject['title'];
    var year = subject['year'];
    return Container(
      child: Row(
        children: <Widget>[
          Icon(
            Icons.play_circle_outline,
            color: Colors.redAccent,
          ),
          Text(
            title,
            style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
          ),
          Text('($year)',
              style: TextStyle(
                  fontSize: 16,
                  fontWeight: FontWeight.bold,
                  color: Colors.grey))
        ],
      ),
    );
  }

  getItemContainerView(var subject) {var imgUrl = subject['images']['medium'];
    return Container(
      width: double.infinity,
      padding: EdgeInsets.all(5.0),
      child: Row(
        children: <Widget>[getImage(imgUrl),
          Expanded(child: getMovieInfoView(subject),
            flex: 1,
          )
        ],
      ),
    );
  }

  // 圆角图片
  getImage(var imgUrl) {
    return Container(
      decoration: BoxDecoration(
          image:
          DecorationImage(image: NetworkImage(imgUrl), fit: BoxFit.cover),
          borderRadius: BorderRadius.all(Radius.circular(5.0))),
      margin: EdgeInsets.only(left: 8, top: 3, right: 8, bottom: 3),
      height: itemHeight,
      width: 100.0,
    );
  }

  getStaring(var stars) {
    return Row(children: <Widget>[RatingBar(stars), Text('$stars')],
    );
  }

  // 电影标题,星标评分,演员简介 Container
  getMovieInfoView(var subject) {var start = subject['rating']['average'];
    return Container(
      height: itemHeight,
      alignment: Alignment.topLeft,
      child: Column(
        children: <Widget>[getTitleView(subject),
          RatingBar(start),
          DescWidget(subject)
        ],
      ),
    );
  }

  //NO.1 图标
  numberWidget(var no) {
    return Container(
      child: Text(
        'No.$no',
        style: TextStyle(color: Color.fromARGB(255, 133, 66, 0)),
      ),
      decoration: BoxDecoration(color: Color.fromARGB(255, 255, 201, 129),
          borderRadius: BorderRadius.all(Radius.circular(5.0))),
      padding: EdgeInsets.fromLTRB(8, 4, 8, 4),
      margin: EdgeInsets.only(left: 12, top: 10),
    );
  }
  @override
  bool get wantKeepAlive => true;
}

// 类别、演员介绍
class DescWidget extends StatelessWidget {
  var subject;

  DescWidget(this.subject);

  @override
  Widget build(BuildContext context) {var casts = subject['casts'];
    var sb = StringBuffer();
    var genres = subject['genres'];
    for (var i = 0; i < genres.length; i++) {sb.write('${genres[i]}');
    }
    sb.write("/");
    List<String> list = List.generate(casts.length, (int index) => casts[index]['name'].toString());

    for (var i = 0; i < list.length; i++) {sb.write('${list[i]}');
    }
    return Container(
      alignment: Alignment.topLeft,
      child: Text(sb.toString(),
        softWrap: true,
        textDirection: TextDirection.ltr,
        style:
        TextStyle(fontSize: 16, color: Color.fromARGB(255, 118, 117, 118)),
      ),
    );
  }
}

class RatingBar extends StatelessWidget {
  double stars;

  RatingBar(this.stars);

  @override
  Widget build(BuildContext context) {List<Widget> startList = [];
    // 实心星星
    var startNumber = stars ~/ 2;
    // 半实心星星
    var startHalf = 0;
    if (stars.toString().contains('.')) {int tmp = int.parse((stars.toString().split('.')[1]));
      if (tmp >= 5) {startHalf = 1;}
    }
    // 空心星星
    var startEmpty = 5 - startNumber - startHalf;

    for (var i = 0; i < startNumber; i++) {
      startList.add(Icon(
        Icons.star,
        color: Colors.amberAccent,
        size: 18,
      ));
    }
    if (startHalf > 0) {
      startList.add(Icon(
        Icons.star_half,
        color: Colors.amberAccent,
        size: 18,
      ));
    }
    for (var i = 0; i < startEmpty; i++) {
      startList.add(Icon(
        Icons.star_border,
        color: Colors.grey,
        size: 18,
      ));
    }
    startList.add(Text(
      '$stars',
      style: TextStyle(color: Colors.grey,),
    ));
    return Container(
      alignment: Alignment.topLeft,
      padding: const EdgeInsets.only(left: 0, top: 8, right: 0, bottom: 5),
      child: Row(children: startList,),
    );
  }
}

附:
1,Flutter 系列教程之环境搭建
2,Flutter 系列教程之学习线路
3,Flutter 系列教程之 Dart 语法
4,Flutter 系列教程之快速入门
5,Flutter 系列教程之 Flutter 1.7 新特性
6,通过 HttpClient 发起 HTTP 请求

退出移动版