Flutter 网络请求之Dio库
- 前言
- 正文
- 一、配置项目
- 二、网络请求
- 三、封装
- ① 单例模式
- ② 网络拦截器
- ③ 返回值封装
- ④ 封装请求
- 四、结合GetX使用
- 五、源码
前言
最近再写Flutter系列文章,在了解过状态管理之后,我们再来学习一下网络请求。
正文
网络请求对于一个线上的App来说是必不可少的,那么Flutter中的网络请求同样也是官方的没有第三方的那么好用,这里我们使用Dio,目前来说比较好用简洁的网络库。
一、配置项目
首先我们创建一个名为study_http
的项目。
创建项目之后,我们配置一下依赖库,在项目的pubspec.yaml
文件中,添加如下所示代码:
dependencies: | |
get: | |
dio: ^5.4.0 |
添加位置如下图所示:
然后点击Pub get
,获取并安装所添加的库,安装成功之后,项目配置完成。
二、网络请求
下面我们来设计一个场景,页面上有一个图片和一个按钮,默认显示一个图片,点击按钮之后请求网络接口,返回一个图片,将这个请求返回的网络图片显示出来,首先我们在lib
下新建一个https
的目录,然后这个目录下新建一个https_page.dart
文件 ,里面代码如下所示:
import 'dart:convert'; | |
import 'package:flutter/material.dart'; | |
import 'package:dio/dio.dart'; | |
import 'package:get/get.dart'; | |
class HttpsPage extends StatelessWidget { | |
var imgPath = | |
"https://img-s-msn-com.akamaized.net/tenant/amp/entityid/BB1h31Ip.img?w=768&h=1226&m=6&x=326&y=887&s=506&d=118" | |
.obs; | |
final dio = Dio(); | |
void request() async { | |
var response = await dio.get('https://www.dmoe.cc/random.php?return=json'); | |
//转化为Json | |
String jsonString = jsonEncode(response.data); | |
print(jsonString); | |
// 解析JSON字符串 | |
Map<String, dynamic> json = jsonDecode(jsonString); | |
// 获取特定字段值 | |
imgPath.value = json['imgurl']; | |
} | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: Container( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Obx(() => Image.network( | |
imgPath.value, | |
width: 200, | |
)), | |
SizedBox(height: 10), | |
ElevatedButton( | |
onPressed: () { | |
request(); | |
}, | |
child: Text("请求网络"), | |
) | |
], | |
), | |
), | |
)); | |
} | |
} |
说明一下这个代码,这里使用了Get库,不了解的可以看看我上一篇文章:Flutter 状态管理之GetX库,创建了一个可观察的变量,然后写了一个请求网络的方法,使用了Dio库的Get请求,请求一个API地址,你可以将这个地址在浏览器中测试,确保它可以返回值。这是我请求的结果,如下图所示:
通过网络请求会返回一个response 对象,我们将对象转换为Json字符串,然后再获取字符串中的imgurl的值,也就是这个图片的网络地址链接,最后再更新这个imgPath的值,Obx()包裹的内容就会刷新,下面我们运行一下看看效果:
这是默认的图片,然后点击一下请求网络的按钮,经过短暂的网络延迟之后就会加载出网络请求返回后的图片,如下图所示:
这个请求返回的图片类似于每日一图,所以变化很大,因此你只要有加载出来就可以,不需要跟我一样。
三、封装
在对Dio库进行进行使用的时候,我们通常会进行封装而不是直接使用。Flutter原生的网络请求是使用HttpClient
,使用起来相当繁琐,因此Dio对于HttpClient进行了封装,那么我们为什么还需要对Dio进行封装呢?这就是考虑到实际中的业务处理了,封装都是针对于实际情况来的,下面我们看看怎么封装这个Dio库。
① 单例模式
在使用网络请求时,通常会有多个网络请求,我们可以写一个单例,将一些基本的内容写在单例里面,写几个方法供其他地方调用,下面我们首先来写一个单例在lib
下新建一个net
包,包下新建一个network_manager.dart
文件,代码如下所示:
import 'package:dio/dio.dart'; | |
/// 网络管理 | |
class NetworkManager { | |
static NetworkManager? _instance = NetworkManager._internal(); | |
late Dio dio; | |
static NetworkManager getInstance() { | |
_instance ??= NetworkManager._internal(); | |
return _instance!; | |
} | |
NetworkManager._internal() { | |
// 配置BaseOptions | |
BaseOptions options = BaseOptions( | |
baseUrl: "", | |
//连接超时 | |
connectTimeout: const Duration(seconds: 15), | |
//接收超时 | |
receiveTimeout: const Duration(seconds: 10), | |
//内容类型 | |
contentType: 'application/json;Charset=UTF-8', | |
//响应数据类型:Json | |
responseType: ResponseType.json); | |
dio = Dio(options); | |
} | |
get(String url, {option, params}) async { | |
Response response; | |
try { | |
response = | |
await dio.get(url, options: Options(responseType: ResponseType.json)); | |
print("response.data:${response.data}"); | |
print("response.data:${response.statusCode}"); | |
print("response.data:${response.statusMessage}"); | |
print("response.data:${response.headers}"); | |
} on Exception catch (e) { | |
print("Get方法出错:${e.toString()}"); | |
} | |
} | |
} |
下面说明一下上面代码,首先我们写了一个getInstance()
方法,在这里面判断_instance
是否为空,为空则NetworkManager._internal()
,对dio进行一些基本的配置,然后初始化dio
对象,不为空则,直接返回_instance
。然后写了一个get()
方法,方法里面就是一个get请求,我们在之前已经页面中已经写好了,同时我们打印一下返回的数据,下面我们在前面的页面中改造一下。修改https_page.dart
中的request()
方法,代码如下所示:
void request() async { | |
NetworkManager.getInstance().get('https://www.dmoe.cc/random.php?return=json'); | |
} |
这里就是直接使用单例中的方法,我们就不需要再当前页面创建dio对象了,运行一下,看控制台日志,如下图所示:
现在我们的方法在单例中有效果,我们继续往下走。
② 网络拦截器
现在的这个日志确实不怎么好看,为了解决这个问题,也为了我们看日志的时候一目了然,我们可以自定义一个拦截器,在net
包下新建一个interceptor
包,该包下新建一个custom_interceptor.dart
文件,里面的代码如下所示:
import 'dart:convert'; | |
import 'package:dio/dio.dart'; | |
import 'package:flutter/foundation.dart'; | |
///日志拦截器 | |
class CustomInterceptor extends Interceptor { | |
void onRequest(RequestOptions options, RequestInterceptorHandler handler) { | |
StringBuffer buffer = StringBuffer(); | |
buffer.write('⌈‾‾ Request ヾ(•ω•`)o \n'); | |
buffer.write('| \n'); | |
buffer.write('| - Url: ${options.baseUrl + options.path}\n'); | |
buffer.write('| - Method:${options.method}\n'); | |
buffer.write('| - Header:${options.headers.toString()}\n'); | |
final data = options.data; | |
if (data != null) { | |
if (data is Map) { | |
buffer.write('| - Body: ${options.data.toString()}\n'); | |
} else if (data is FormData) { | |
final formDataMap = {} | |
..addEntries(data.fields) | |
..addEntries(data.files); | |
buffer.write("| - Body: ${formDataMap.toString()}\n"); | |
} else { | |
buffer.write("| - Body: ${data.toString()}\n"); | |
} | |
} | |
buffer.write( | |
'⌊_____________________________________________________________________'); | |
printDebugLog(buffer); | |
return handler.next(options); | |
} | |
void onResponse(Response response, ResponseInterceptorHandler handler) { | |
StringBuffer buffer = StringBuffer(); | |
buffer.write('⌈‾‾ Response O(∩_∩)O \n'); | |
buffer.write('| \n'); | |
buffer.write('| - Code: ${response.statusCode}\n'); | |
buffer.write('| - CodeMsg:${response.statusMessage}\n'); | |
buffer.write('| - Header:\n'); | |
response.headers.forEach((key, value) { | |
buffer.write('| $key $value\n'); | |
}); | |
final data = response.data; | |
if (data != null) { | |
if (data is Map) { | |
buffer.write('| - Data: ${response.data.toString()}\n'); | |
String dataJson = jsonEncode(response.data); | |
buffer.write('| - Json: $dataJson\n'); | |
} else if (data is FormData) { | |
final formDataMap = {} | |
..addEntries(data.fields) | |
..addEntries(data.files); | |
buffer.write("| - Data: ${formDataMap.toString()}\n"); | |
} else { | |
buffer.write("| - Data: ${data.toString()}\n"); | |
} | |
} | |
buffer.write( | |
'⌊_____________________________________________________________________'); | |
printDebugLog(buffer); | |
return handler.next(response); | |
} | |
void onError(DioException err, ErrorInterceptorHandler handler) { | |
//处理错误信息 | |
handlerError(err); | |
StringBuffer buffer = StringBuffer(); | |
buffer.write('⌈‾‾ Error (っ °Д °;)っ\n'); | |
buffer.write('| \n'); | |
buffer.write('| - ExceptionType:${err.type.name}\n'); | |
buffer.write('| - ErrorMsg: ${err.message}\n'); | |
buffer.write('| - StatusCode: ${err.response?.statusCode}\n'); | |
buffer.write('| - StatusMsg: ${err.response?.statusMessage}\n'); | |
buffer.write( | |
'⌊_____________________________________________________________________'); | |
printDebugLog(buffer); | |
return handler.next(err); | |
} | |
///处理错误信息 --自行去实现里面的功能代码 | |
void handlerError(DioException err) { | |
switch (err.type) { | |
//连接超时 | |
case DioExceptionType.connectionTimeout: | |
break; | |
//响应超时 | |
case DioExceptionType.receiveTimeout: | |
break; | |
//发送超时 | |
case DioExceptionType.sendTimeout: | |
break; | |
//请求取消 | |
case DioExceptionType.cancel: | |
break; | |
//错误响应 404 等 | |
case DioExceptionType.badResponse: | |
break; | |
//错误证书 | |
case DioExceptionType.badCertificate: | |
break; | |
//未知错误 | |
default: | |
break; | |
} | |
} | |
void printDebugLog(StringBuffer buffer) { | |
if (kDebugMode) { | |
print(buffer.toString()); | |
} | |
} | |
} |
在这里面我们继承了创建CustomInterceptor
类,继承Dio
的Interceptor
,重写里面onRequest(请求前)
、onResponse(响应前)
、onError(错误时)
的拦截方法,在里面对于相关数据信息进行打印,同时只在debug模式下打印,下面我们回到NetworkManager中,使用这个自定义拦截器。
import 'interceptor/custom_interceptor.dart';
首先导包,然后在_internal()方法中增加如下代码:
//添加日志拦截器 | |
dio.interceptors.add(CustomInterceptor()); |
添加位置如下图所示:
再将get方法中的打印注释掉
然后我们重新运行一下,请求网络接口,查看控制台日志,如下图所示:
这样看起来是否会清晰一些呢,可以自行调整,我们接着往下走。
③ 返回值封装
对返回值的封装,我们可以分为两步,第一步就是在响应前封装,第二步在响应后转换。先来看第一步,在net
包下新建一个response
包,该包下新建一个base_response.dart
,代码如下所示:
///自定义响应封装 | |
class BaseResponse<T> { | |
//状态码 | |
final int? code; | |
//状态描述 | |
final String? msg; | |
//数据 | |
final T data; | |
BaseResponse({required this.code,required this.msg,required this.data}); | |
String toString() { | |
StringBuffer buffer = StringBuffer(); | |
buffer.write('{'); | |
buffer.write('"code":$code'); | |
buffer.write('"msg":"$msg"'); | |
buffer.write('"data":"$data"'); | |
buffer.write('}'); | |
return super.toString(); | |
} | |
} |
这里就是对默认的Response进行一次封装,然后这里的data就是我们接口所拿到的返回值, 下面我们改动一下之前的自定义拦截器custom_interceptor.dart
中的代码,主要就是修改onResponse()
方法,代码如下:
void onResponse(Response response, ResponseInterceptorHandler handler) { | |
//返回自定义的Base | |
final baseResponse = BaseResponse(code: response.statusCode, msg: response.statusMessage, data: response.data); | |
response.data = baseResponse; | |
StringBuffer buffer = StringBuffer(); | |
buffer.write('⌈‾‾ Response O(∩_∩)O \n'); | |
buffer.write('| \n'); | |
buffer.write('| - Code: ${response.statusCode}\n'); | |
buffer.write('| - CodeMsg:${response.statusMessage}\n'); | |
buffer.write('| - Header:\n'); | |
response.headers.forEach((key, value) { | |
buffer.write('| $key $value\n'); | |
}); | |
final data = response.data; | |
if (data != null) { | |
if (data is Map) { | |
buffer.write('| - Data: ${response.data.toString()}\n'); | |
String dataJson = jsonEncode(response.data); | |
buffer.write('| - Json: $dataJson\n'); | |
} else if (data is FormData) { | |
final formDataMap = {} | |
..addEntries(data.fields) | |
..addEntries(data.files); | |
buffer.write("| - Data: ${formDataMap.toString()}\n"); | |
} else { | |
buffer.write("| - Data: ${baseResponse.data.toString()}\n"); | |
} | |
} | |
buffer.write( | |
'⌊_____________________________________________________________________'); | |
printDebugLog(buffer); | |
return handler.next(response); | |
} |
核心代码就是这一段
将response.data封装到BaseResponse中,然后再赋值返回。然后我们再对返回值进行一个JSON转Bean的操作,AS中提供了一个插件,FlutterJsonBeanFactory
,安装。
安装好之后,我们可以重启一下AS,然后就来根据JSON返回值构建Dart的Bean。在lib
包下新建一个model
包,然后鼠标右键model
包,点击New → JsonToDartBeanAction
,如下图所示:
输入文件名称,然后将接口返回的JOSN:
{ | |
"code": "200", | |
"imgurl": "https://image.baidu.com/search/down?url=https://tvax3.sinaimg.cn//large/a15b4afegy1fmvk16yis3j21hc0u0tpx.jpg", | |
"width": "1920", | |
"height": "1080" | |
} |
赋值进去,如下图所示:
点击Make,完成构建。
构建之后会在model包下生成一个img_entity.dart,我刚才输入的是img,_entity是这个插件自己添加的,然后会生成一个generated文件夹,里面可以看到一个img_entity.g.dart文件,里面的内容就是对你JSON和Bean之间的转化代码的生成,我们不需要关心。先不急着使用这个返回值,我们继续往下走。
④ 封装请求
接着我们封装请求方法,针对网络请求有get、post、put等等方式,在dio库中,最终实际上调用的都是request请求,在net包下新建一个method包,该包下新建一个bese_method.dart,代码如下:
enum BaseMethod { | |
get, | |
post, | |
put, | |
delete, | |
patch, | |
head | |
} |
这里代码很简单,就是列举了dio的网络请求方式,然后我们回到network_manager.dart中,在里面新增一个request()
方法,其他的代码也做了修改,整体代码如下所示:
class NetworkManager { | |
factory NetworkManager() => _getInstance(); | |
static NetworkManager? _instance = NetworkManager._initialize(); | |
late Dio _dio; | |
static NetworkManager _getInstance() { | |
_instance ??= NetworkManager._initialize(); | |
return _instance!; | |
} | |
NetworkManager._initialize() { | |
// 配置BaseOptions | |
BaseOptions options = BaseOptions( | |
baseUrl: "", | |
//连接超时 | |
connectTimeout: const Duration(seconds: 15), | |
//接收超时 | |
receiveTimeout: const Duration(seconds: 10), | |
//内容类型 | |
contentType: 'application/json;Charset=UTF-8', | |
//响应数据类型:Json | |
responseType: ResponseType.json); | |
_dio = Dio(options); | |
//添加日志拦截器 | |
_dio.interceptors.add(CustomInterceptor()); | |
} | |
///网络请求 | |
Future<T> request<T>(String path, | |
{BaseMethod method = BaseMethod.get, Map<String, dynamic>? params, | |
data, Options? options}) async { | |
const methodValues = { | |
BaseMethod.get: 'get', | |
BaseMethod.post: 'post', | |
BaseMethod.put: 'put', | |
BaseMethod.delete: 'delete', | |
BaseMethod.patch: 'patch', | |
BaseMethod.head: 'head', | |
}; | |
options ??= Options(method: methodValues[method]); | |
try { | |
Response response; | |
response = await _dio.request(path, | |
data: data, queryParameters: params, options: options); | |
return response.data; | |
} on DioException catch (e) { | |
throw e; | |
} | |
} | |
} |
下面我们再回到https_page.darat中去使用,修改request()方法,代码如下所示:
void request() async { | |
BaseResponse response = await NetworkManager().request('https://www.dmoe.cc/random.php?return=json'); | |
ImgEntity imgEntity = ImgEntity.fromJson(response.data); | |
imgPath.value = imgEntity.imgurl; | |
} |
如果有报错注意一下导包
import '../model/img_entity.dart';
运行一下,效果和之前是一样的,然后我们再来改动一下,针对于这个API地址:
https://www.dmoe.cc/random.php?return=json
我们可以分为两部分。
基础地址
https://www.dmoe.cc/
功能地址
random.php?return=json
一般的项目中,基础地址不会经常变,也就是ip地址,而不同的功能会根据实际情况去改变接口,因此这一部分我们需要和实际方法进行绑定,下面我们在NetworkManager
中增加一行代码:
final _mBaseUrl = "https://www.dmoe.cc/";
然后修改baseUrl的值,之前是空字符串,如下图所示:
再去修改实际调用的地方,如下图所示:
这样就对一个API地址进行了分离,这在实际开发中是很常见的做法。对于dio的封装就到这里了,肯定不是完善了,因为还有很多东西没有考虑到,我们可以根据实际中的需要再去添加,我这里就不赘述了,下面我们结合GetX去使用。
四、结合GetX使用
在https包下新建一个https_controller.dart,代码如下:
import 'package:get/get.dart'; | |
import '../model/img_entity.dart'; | |
import '../net/network_manager.dart'; | |
import '../net/response/base_response.dart'; | |
class HttpsController extends GetxController { | |
var imgPath = | |
"https://img-s-msn-com.akamaized.net/tenant/amp/entityid/BB1h31Ip.img?w=768&h=1226&m=6&x=326&y=887&s=506&d=118" | |
.obs; | |
void request() async { | |
BaseResponse response = await NetworkManager().request('random.php?return=json'); | |
ImgEntity imgEntity = ImgEntity.fromJson(response.data); | |
imgPath.value = imgEntity.imgurl; | |
} | |
} |
这里就是将网络请求相关的变量和方法都放到HttpsController 中,然后我们再回到HttpsPage,修改代码如下所示:
import 'package:flutter/material.dart'; | |
import 'package:get/get.dart'; | |
import 'https_controller.dart'; | |
class HttpsPage extends StatelessWidget { | |
final httpsController = Get.put(HttpsController()); | |
@override | |
Widget build(BuildContext context) { | |
return Scaffold( | |
body: Center( | |
child: Container( | |
child: Column( | |
mainAxisAlignment: MainAxisAlignment.center, | |
children: [ | |
Obx(() => Image.network( | |
httpsController.imgPath.value, | |
width: 200, | |
)), | |
SizedBox(height: 10), | |
ElevatedButton( | |
onPressed: () => httpsController.request(), | |
child: Text("请求网络"), | |
) | |
], | |
), | |
), | |
)); | |
} | |
} |
主要改动地方如下图所示:
这样基本上就符合现在的开发理念了,数据和UI进行分离,再次运行,效果依然一样,好了,本篇文章就到这里。
五、源码
源码地址:study_http