Flutter開發系列教程之網絡請求

NO IMAGE

簡介

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官方地址為:pub.dartlang.org,如下圖所示:

Flutter開發系列教程之網絡請求

1.1 安裝依賴

使用Dart的原生http庫進行網絡請求時,需要先在Dart PUB或官方Github裡把相關的http庫引用下來,然後才能使用。添加包依賴前,我們可以使用https://pub.dev/packages/http來查看依賴包的版本和使用方法。

Flutter開發系列教程之網絡請求

然後,在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;
});
}
}

運行上面的代碼,結果如下圖:

Flutter開發系列教程之網絡請求

除了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');
}
}
}

執行上面的代碼,結果如下圖:

Flutter開發系列教程之網絡請求

利用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的基本使用,最終效果如圖:

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

相關文章

Flutter開發之基礎Widgets

《WEEX跨平臺開發實戰》出版啦

Flutter開發之導航與路由管理

GitHub標星8W,Google面試指南