該文已授權(quán)公眾號(hào) 「碼個(gè)蛋」以清,轉(zhuǎn)載請(qǐng)指明出處
前面講完了常用的部件儿普,BLoC
模式,數(shù)據(jù)持久化等常用的掷倔,今天再介紹個(gè)重頭戲 —— 網(wǎng)絡(luò)請(qǐng)求
HttpClient
HttpClient
是 dart
自帶的網(wǎng)絡(luò)請(qǐng)求方式眉孩,在 dart:io
包下。使用 HttpClient
作為請(qǐng)求分以下幾個(gè)步驟
-
創(chuàng)建 HttpClient 實(shí)例
HttpClient client = HttpClient();
-
打開連接勒葱,并設(shè)置一些頭參數(shù)浪汪,請(qǐng)求參數(shù)等
// 如果 url 中沒有查詢參數(shù)可直接創(chuàng)建 Uri uri = Uri.parse('https://www.xxx.com'); // 如果存在查詢參數(shù)則在 Uri 中添加 Uri uri = Uri(scheme: 'https', host: 'www.xxx.com', queryParameters: {'a': 'AAA'}); // 打開連接 HttpClientRequest request = await client.getUrl(uri); request.headers.add('token', 'Bear ${'x' * 20}'); // 添加頭部 token 信息 // 如果是 post 或者 put 請(qǐng)求,通過 `add` 添加請(qǐng)求體 // 因?yàn)?`add` 方法需要傳入 `List<int>` 參數(shù)凛虽,可以通過 utf8.encode 進(jìn)行編碼 request.add(utf8.encode('{"a": "aaa"}')); // 也可以通過添加流的方式進(jìn)行添加 request.addStream(input);
-
連接服務(wù)器
// 設(shè)置 request 后通過 request.close() 獲取一個(gè)響應(yīng)對(duì)象 HttpClientResponse死遭, // 包括響應(yīng)頭,響應(yīng)內(nèi)容等 HttpClientResponse response = await request.close();
-
讀取服務(wù)器響應(yīng)內(nèi)容
String responseBody = await response.transform(utf8.decoder).join();
-
關(guān)閉實(shí)例
client.close();
例如我們要去請(qǐng)求 Bird.so
的首頁并顯示凯旋,我們可以這么實(shí)現(xiàn)
_httpClientRequest() async {
HttpClient client;
// try catch finally 用于捕獲請(qǐng)求過程中發(fā)生的異常呀潭,在 finally 中設(shè)置保證 client 能夠關(guān)閉
try {
client = HttpClient();
HttpClientRequest request = await client.getUrl(Uri.parse(_BIRD_SO_URL));
HttpClientResponse response = await request.close();
String strResponse = await response.transform(utf8.decoder).join();
setState(() => _netBack = strResponse);
} catch (e) {
print('${e.toString()}');
setState(() => _netBack = 'Fail');
} finally {
client.close();
}
}
最后實(shí)現(xiàn)的效果
很顯然,用 HttpClient
請(qǐng)求相對(duì)來說是個(gè)非常麻煩的過程至非,如果要涉及到文本上傳之類的钠署,那么就會(huì)更麻煩了,所以這邊引入一個(gè)網(wǎng)絡(luò)請(qǐng)求的插件 dio
荒椭,寫本文的時(shí)候版本為 2.1.0
Dio
dio 是個(gè)非常強(qiáng)大的網(wǎng)絡(luò)請(qǐng)求庫(kù)谐鼎,他的方式類似 OkHttp
,我們可以直接查看官方文檔趣惠,使用方式非常簡(jiǎn)單狸棍,創(chuàng)建一個(gè) Dio
實(shí)例身害,然后就可以通過 get
,post
等方式發(fā)起請(qǐng)求隔缀,返回 Future<Response>
题造,而且支持多個(gè)并發(fā)請(qǐng)求,可以設(shè)置返回響應(yīng)的類型猾瘸,監(jiān)聽上傳下載進(jìn)度等等界赔,看著就很給力。對(duì)于簡(jiǎn)單的方式牵触,這邊就不做太多介紹淮悼,主要講下攔截器,也是非常給力的一部分揽思。比如我們需要請(qǐng)求這么個(gè)接口 https://randomuser.me/api/
這個(gè)接口通過 get
請(qǐng)求袜腥,可以加入任意的查詢參數(shù)。比如我們需要實(shí)現(xiàn)一個(gè)請(qǐng)求加解密的過程钉汗,如果每次都在上傳參數(shù)或者返回請(qǐng)求的時(shí)候去加密羹令,解密的話,就做了非常多無用功了损痰,那么這時(shí)候攔截器就派上用場(chǎng)了福侈。先定義下加解密的規(guī)則,上傳的參數(shù)統(tǒng)一轉(zhuǎn)為小寫卢未,不存在大寫肪凛,請(qǐng)求回的數(shù)據(jù),不能含有 info
字段辽社∥扒剑看下如何實(shí)現(xiàn)
_dioRequest() async {
BaseOptions options = BaseOptions(connectTimeout: 5000, receiveTimeout: 60000);
Dio dio = Dio(options);
dio.interceptors.add(InterceptorsWrapper(onRequest: (opt) {
// 獲取查詢的參數(shù)
Map params = opt.queryParameters;
// 將所有的參數(shù)轉(zhuǎn)為小寫,因?yàn)椴樵儏?shù)通過 map 形式上傳
params.forEach((key, value) =>
opt.queryParameters[key] = '$value'.toLowerCase());
// 這邊還可以做些別的操作滴铅,例如需要 token 進(jìn)行用戶身份驗(yàn)證戳葵,則通過頭部進(jìn)行添加
// opt.headers['authorization'] = 'token';
// 在官網(wǎng)中,提供了 lock 和 unlock 的寫法汉匙,被 lock 后拱烁,接下來的請(qǐng)求會(huì)進(jìn)入隊(duì)列等待,
// 直到 unlock 后才能繼續(xù)盹兢,可以用于幾個(gè)請(qǐng)求,后續(xù)的需要用到前面的返回值的情況使用
// 返回修改后的 RequestOptions
return opt;
}, onResponse: (resp) {
// 返回響應(yīng)體后守伸,將 info 字段的內(nèi)容切除绎秒,并將 json 拼接完成
resp.data = '${'${resp.data}'.split(', info').first}}';
return resp;
}, onError: (error) {
// 發(fā)生錯(cuò)誤時(shí)的回調(diào)
return error;
}));
// 發(fā)送一個(gè)請(qǐng)求,可以查看下打印的結(jié)果
Response response = await dio.get(_USER_ME_URL, queryParameters: {'a': 'AAA', 'b': 'BbBbBb'});
print(response.data);
print(response.request.headers);
print(response.request.queryParameters);
setState(() => _netBack = response.data.toString()); // 界面顯示 response.data
}
看下最后的顯示信息
請(qǐng)求體的頭部成功加上了 authorization
參數(shù)尼摹,請(qǐng)求的參數(shù)全部變?yōu)樾懠郏祷氐男畔⒁舶?info 字段值去除剂娄。在很多時(shí)候,請(qǐng)求接口后玄呛,需要將 json 轉(zhuǎn)換成 pojo 類來處理阅懦,可以通過 json_serializable
這個(gè)三方插件實(shí)現(xiàn),這邊提供文章 Flutter Json自動(dòng)反序列化徘铝,當(dāng)然這種方式比較麻煩耳胎,這里推薦個(gè) Android Studio
下的插件 dart_json_format
直接搜索就可以,如果用的是 Vitual Code
或者別的不是 JetBrains
系列的惕它,這里有個(gè)轉(zhuǎn)換的網(wǎng)址 JsonToDart怕午。
以上代碼查看 http_main.dart
文件
實(shí)踐一下下
不知道小伙還記得前面講的 BLoC
沒有,忘了可以查看 Flutter 狀態(tài)管理及 BLoC淹魄,這里結(jié)合 BLoC
和 Dio
實(shí)現(xiàn)界面和邏輯分離的小例子郁惜,接口使用前面提到的 https://randomuser.me/api/
接口。網(wǎng)絡(luò)應(yīng)該是比較常用的甲锡,所以對(duì)其進(jìn)行一些封裝還是很有必要的兆蕉,這邊提供下我自己封裝的方法
import 'package:dio/dio.dart';
// 用于錯(cuò)誤信息回調(diào)
typedef ErrorCallback = void Function(String msg);
class HttpUtils {
static const GET = 'get';
static const POST = 'post';
static Dio _dio;
static HttpUtils _instance;
Dio get hp => _dio;
// dio 可以在 BaseOptions 中指定域名 baseUrl,
// 后續(xù)接口就不需要再添加域名了
// 如果請(qǐng)求的接口域名發(fā)生了變化缤沦,只要把全部 url 寫全虎韵,就會(huì)自動(dòng)使用新的域名
HttpUtils._internal(String base) {
// 生成一個(gè)單例,防止多次打開關(guān)閉造成開銷
_dio = Dio(BaseOptions(baseUrl: base, connectTimeout: 10000, receiveTimeout: 10000));
}
factory HttpUtils(String base) {
if (_instance == null) _instance = HttpUtils._internal(base);
return _instance;
}
// 添加攔截器
addInterceptor(List<InterceptorsWrapper> interceptors) {
_dio.interceptors.clear();
_dio.interceptors.addAll(interceptors);
}
Future<Response<T>> getRequest<T>(url, {Map params, ErrorCallback callback}) =>
_request(url, GET, params: params, callback: callback);
Future<Response<T>> postRequest<T>(url, {Map params, ErrorCallback callback}) =>
_request(url, POST, params: params, callback: callback);
Future<Response> download(url, path, {ProgressCallback receive, CancelToken token}) =>
_dio.download(url, path, onReceiveProgress: receive, cancelToken: token);
// T 可以指定返回的類型疚俱,String 或者 Map<String, dynamic>
Future<Response<T>> _request<T>(
url,
String method, {
Map params, // 上傳的參數(shù)
Options opt,
ErrorCallback callback, // 錯(cuò)誤回調(diào)
ProgressCallback send, // 上傳進(jìn)度監(jiān)聽
ProgressCallback receive, // 下載監(jiān)聽
CancelToken token, // 用于取消的 token劝术,可以多個(gè)請(qǐng)求綁定一個(gè) token
}) async {
try {
Response<T> rep;
if (method == GET) {
// 如果不是重新創(chuàng)建 Dio 實(shí)例,get 方法使用 queryParams 會(huì)出錯(cuò)呆奕,不懂原因养晋,使用拼接沒有問題
if (params != null && params.isNotEmpty) {
var sb = StringBuffer('?');
params.forEach((key, value) {
sb.write('$key=$value&');
});
// get 請(qǐng)求下拼接路徑
url += sb.toString().substring(0, sb.length - 1);
}
rep = await _dio.get(url, options: opt, onReceiveProgress: receive, cancelToken: token);
} else if (method == POST) {
// post 參數(shù)放請(qǐng)求體
rep = params == null
? await _dio.post(url, options: opt, cancelToken: token, onSendProgress: send, onReceiveProgress: receive)
: await _dio.post(url,
data: params, options: opt, cancelToken: token, onSendProgress: send, onReceiveProgress: receive);
}
// 如果 statusCode 不是 200 則錯(cuò)誤回調(diào),返回空的 Response
if (rep.statusCode != 200 && callback != null) {
callback('network error, and code is ${rep.statusCode}');
return null;
}
return rep;
} catch (e) {
if (callback != null) {
callback('network error, catch error: ${e.toString()}');
}
return null;
}
}
}
封裝后就可以愉快的調(diào)用了梁钾,如果有別的請(qǐng)求方式后期可以繼續(xù)擴(kuò)展绳泉。繼續(xù)看代碼,創(chuàng)建一個(gè) application.dart
文件姆泻,用于存放全局參數(shù)
class Application {
static HttpUtils http;
}
并在 main()
方法中進(jìn)行初始化,接下來就可以直接使用
void main() {
Application.http = HttpUtils('https://randomuser.me');
runApp(DemoApp());
// 透明狀態(tài)欄
if (Platform.isAndroid) {
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(statusBarColor: Colors.transparent));
}
}
看下最后的實(shí)現(xiàn)效果吧拇勃,剛進(jìn)入沒有數(shù)據(jù)則通過轉(zhuǎn)圈圈提示四苇,加載完數(shù)據(jù)后,點(diǎn)擊頭像更換下個(gè)
實(shí)現(xiàn) BLoC
需要有一個(gè)管理類
class UserBloc extends BaseBloc {
RandomUserModel _user;
RandomUserModel get user => _user;
BehaviorSubject<RandomUserModel> _controller = BehaviorSubject();
Observable<RandomUserModel> get stream => Observable(_controller.stream);
// 網(wǎng)絡(luò)請(qǐng)求獲取新的數(shù)據(jù)方咆,并更新
updateUserInfo() {
Application.http.getRequest('/api').then((response) {
// RandomUserModel 就是接口返回的 json 轉(zhuǎn)成的 model 類
RandomUserModel model = RandomUserModel.fromMap(response.data);
_user = model;
// add 到 controller 通知修改
_controller.add(model);
});
}
@override
void dispose() {
_controller?.close(); // 及時(shí)銷毀
}
}
設(shè)置好管理類后月腋,就可以來編寫界面了,界面也比較簡(jiǎn)單
class UserPageDemo extends StatelessWidget {
// 將首字母大寫
String _upperFirst(String content) {
assert(content != null && content.isNotEmpty);
return '${content.substring(0, 1).toUpperCase()}${content.substring(1)}';
}
// 地址信息通用部件
Widget _userLocation(String info) => Padding(
padding: const EdgeInsets.only(top: 4.0),
child: Text(info, style: TextStyle(color: Colors.white, fontSize: 16.0)));
@override
Widget build(BuildContext context) {
UserBloc _bloc = BlocProvider.of<UserBloc>(context);
_bloc.updateUserInfo();
return Scaffold(
// StreamBuilder 接受更新數(shù)據(jù)的 stream
body: StreamBuilder(
builder: (_, AsyncSnapshot<RandomUserModel> snapshot) => Container(
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue[600], Colors.blue[400]])),
child: !snapshot.hasData
? CupertinoActivityIndicator(radius: 12.0)
: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
InkWell( // 用于切換數(shù)據(jù)
child: ClipOval( // 圓形頭像
child: FadeInImage.assetNetwork(
placeholder: 'images/ava_default.png', image: snapshot.data.results[0].picture.large),
),
onTap: () => _bloc.updateUserInfo()), // 更新數(shù)據(jù)
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: Text(
'${_upperFirst(snapshot.data.results[0].name.first)} ${_upperFirst(snapshot.data.results[0].name.last)}',
style: TextStyle(color: Colors.white, fontSize: 24.0)),
),
Text('${snapshot.data.results[0].email}',
style: TextStyle(color: Colors.white, fontSize: 18.0)),
_userLocation('${snapshot.data.results[0].location.street}'),
_userLocation('${_upperFirst(snapshot.data.results[0].location.city)}'),
_userLocation('${_upperFirst(snapshot.data.results[0].location.state)}'),
]),
),
initialData: _bloc.user, // 注入初始值
stream: _bloc.stream), // 注入更新 stream
);
}
}
以上代碼查看 bloc_network
包下的所有文件
當(dāng)然了,福利是不可少的榆骚,但是需要你到項(xiàng)目中自己去找片拍。差不多入門的部分就講到這了,接下來考慮加個(gè)實(shí)戰(zhàn)妓肢,總之先等等吧捌省,我找個(gè)好的題材接口來寫。
最后代碼的地址還是要的:
文章中涉及的代碼:demos
基于郭神
cool weather
接口的一個(gè)項(xiàng)目碉钠,實(shí)現(xiàn)BLoC
模式纲缓,實(shí)現(xiàn)狀態(tài)管理:flutter_weather一個(gè)課程(當(dāng)時(shí)買了想看下代碼規(guī)范的,代碼更新會(huì)比較慢放钦,雖然是跟著課上的一些寫代碼色徘,但是還是做了自己的修改,很多地方看著不舒服操禀,然后就改成自己的實(shí)現(xiàn)方式了):flutter_shop
如果對(duì)你有幫助的話褂策,記得給個(gè) Star,先謝過颓屑,你的認(rèn)可就是支持我繼續(xù)寫下去的動(dòng)力~