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请求主要是执行获取资源操作的,例如通过URL从服务器获取返回的资源,其中GET可以把请求的一些参数信息拼接在URL上,传递给服务器,由服务器端进行参数信息解析,服务器收到请求后返回相应的资源给请求者。注意:GET请求拼接的URL数据大小和长度是有最大限制的,传输的数据量一般限制在2KB。
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请求主要用于给请求的客户端返回头信息,而不返回Body主体内容。和GET方式类似,只不过GET方式有Body实体返回,而HEAD只返回头信息,无Body实体内容返回。主要是用于确认URL的有效性、资源更新的日期时间、查看服务器状态等等,对于有这方面需求的请求来说,比较不占用资源。
PUT请求主要用于执行传输文件操作,类似于FTP的文件上传一样,请求里包含文件内容,并将此文件保存到URI指定的服务器位置。
和POST方式的主要区别是:PUT请求方式如果前后两个请求相同,则后一个请求会把前一个请求覆盖掉,实现了PUT方式的修改资源;而POST请求方式如果前后两个请求相同,则后一个请求不会把前一个请求覆盖掉,实现了POST的增加资源。
DELETE请求主要用于执行删除操作,告诉服务器想要删除的资源,不常用。
OPTIONS请求主要用于执行查询针对所要请求的URI资源服务器所支持的请求方式,也就是获取这个URI所支持客户端提交给服务器端的请求方式有哪些。
TRACE请求主要用于执行追踪传输路径的操作,例如,我们发起了一个Http请求,在这个过程中这个请求可能会经过很多个路径和过程,TRACE就是告诉服务器在收到请求后,返回一条响应信息,将它收到的原始Http请求信息返回给客户端,这样就可以验证在Http传输过程中请求是否被修改过。
CONNECT请求主要用于执行连接代理操作,例如“翻墙”。客户端通过CONNECT方式与服务器建立通信隧道,进行TCP通信。主要通过SSL和TLS安全传输数据。CONNECT的作用就是告诉服务器让它代替客户端去请求访问某个资源,然后再将数据返回给客户端,相当于一个媒介中转。
Dart原生http请求库是Dart提供的一种请求方式,常见的请求方式都支持,除此之外,还支持如上传和下载文件等操作。
Dart官方仓库提供了大量的三方库和官方库,引用也非常的方便,Dart PUB官方地址为: https://pub.dartlang.org ,如下图所示:
使用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;
http库支持常见的get、post、del等请求。其中,get请求的格式如下:
get(dynamic url, { Map<String, String> headers }) → Future<Response>
post请求的格式如下:
post(dynamic url, { Map<String, String> headers, dynamic body, Encoding encoding }) → Future<Response>
例如,下面是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错误'); });
例如,下面使用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(() { /// ... /// }); }
Dart IO库中提供的HttpClient可以实现一些基本的Http请求。不过,HttpClient只能实现一些基本的网络请求,对应一些复杂的网络请求还无法完成,如POST里的Body请求体传输内容类型部分还无法支持,multipart/form-data这个类型传输还不支持。
使用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();
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'); } } }
执行上面的代码,结果如下图:
除了上面两种常见的请求方式外,Flutter开发中还可以使用dio等第三方库来实现Http网络请求,如Dart社区提供的dio库。
前面说过,HttpClient发起网络请求是比较麻烦的,很多事情都需要我们手动处理,如果再涉及到文件上传/下载、Cookie管理等就会非常繁琐。而Dart社区有一些第三方http请求库,就可以简化这些操作。dio库不仅支持常见的网络请求,还支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等操作。
和使用其他的第三方库一样,使用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应该使用单例模式。
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);
response=await dio.post("/test",data:{"id":12,"name":"wendu"})
如果要发起多个并发请求,可以使用下面的方式:
response= await Future.wait([dio.post("/info"),dio.get("/token")]);
如果要下载文件,可以使用dio的download函数,如下所示:
response=await dio.download("https://www.google.com/",_savePath);
如果要发起表单请求,可以使用下面的方式:
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)
值得一提的是,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; }; };
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请求