4

简介

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请求


xiangzhihong
5.9k 声望15.3k 粉丝

著有《React Native移动开发实战》1,2,3、《Kotlin入门与实战》《Weex跨平台开发实战》、《Flutter跨平台开发与实战》1,2和《Android应用开发实战》