Flutter Dio 亲妈级别封装教程
作者:油糕
来源:SegmentFault 思否社区
前不久看到 艾维码 大佬的dio封装,经过摸索,改吧改吧,使用的不错。对于之前 艾维码 大佬文章中一些已经失效的做了修正
为什么一定要封装一手?
token拦截,错误拦截,统一错误处理,统一缓存,信息封装(错误,正确)
Cookie???滚犊子
不管cookie,再见
全局初始化,传入参数
dio初始化,传入baseUrl, connectTimeout, receiveTimeout,options,header 拦截器等。dio初始化的时候允许我们传入的一些配置
dio初始化的配置
这里说下,之前 艾维码 大佬的帖子中的options,最新版的dio已经使用requestOptions, 之前的merge,现在使用copyWith。详情向下看
如果要白嫖完整的方案
可以参考使用这套方案开发的 flutter + getx 仿开眼视频app,有star的大佬可以赏点star。
项目地址 https://github.com/abcd498936590/flutter_eyepetizer
apk下载 https://pan.baidu.com/share/init?surl=5T13BzbwG_NI8pbaVISoDA
提取码:3ev2
初始化
这里说下拦截器,可以在初始化的时候传入,也可以手写传入,例如我这里定义了四个拦截器,第一个用于全局request时候给请求投加上context-type:json。第二个是全局错误处理拦截器,下面的内容会介绍拦截器部分。
cache拦截器,全局处理接口缓存数据,retry重试拦截器(我暂时没怎么用)
·class Http {
static final Http _instance = Http._internal();
// 单例模式使用Http类,
factory Http() => _instance;
static late final Dio dio;
CancelToken _cancelToken = new CancelToken();
Http._internal() {
// BaseOptions、Options、RequestOptions 都可以配置参数,优先级别依次递增,且可以根据优先级别覆盖参数
BaseOptions options = new BaseOptions();
dio = Dio(options);
// 添加request拦截器
dio.interceptors.add(RequestInterceptor());
// 添加error拦截器
dio.interceptors.add(ErrorInterceptor());
// // 添加cache拦截器
dio.interceptors.add(NetCacheInterceptor());
// // 添加retry拦截器
dio.interceptors.add(
RetryOnConnectionChangeInterceptor(
requestRetrier: DioConnectivityRequestRetrier(
dio: dio,
connectivity: Connectivity(),
),
),
);
// 在调试模式下需要抓包调试,所以我们使用代理,并禁用HTTPS证书校验
// if (PROXY_ENABLE) {
// (dio.httpClientAdapter as DefaultHttpClientAdapter).onHttpClientCreate =
// (client) {
// client.findProxy = (uri) {
// return "PROXY $PROXY_IP:$PROXY_PORT";
// };
// //代理工具会提供一个抓包的自签名证书,会通不过证书校验,所以我们禁用证书校验
// client.badCertificateCallback =
// (X509Certificate cert, String host, int port) => true;
// };
// }
}
///初始化公共属性
///
/// [baseUrl] 地址前缀
/// [connectTimeout] 连接超时赶时间
/// [receiveTimeout] 接收超时赶时间
/// [interceptors] 基础拦截器
void init({
String? baseUrl,
int connectTimeout = 1500,
int receiveTimeout = 1500,
Map<String, String>? headers,
List<Interceptor>? interceptors,
}) {
dio.options = dio.options.copyWith(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
headers: headers ?? const {},
);
// 在初始化http类的时候,可以传入拦截器
if (interceptors != null && interceptors.isNotEmpty) {
dio.interceptors..addAll(interceptors);
}
}
// 关闭dio
void cancelRequests({required CancelToken token}) {
_cancelToken.cancel("cancelled");
}
// 添加认证
// 读取本地配置
Map<String, dynamic>? getAuthorizationHeader() {
Map<String, dynamic>? headers;
// 从getx或者sputils中获取
// String accessToken = Global.accessToken;
String accessToken = "";
if (accessToken != null) {
headers = {
'Authorization': 'Bearer $accessToken',
};
}
return headers;
}
Future get(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
bool refresh = false,
bool noCache = !CACHE_ENABLE,
String? cacheKey,
bool cacheDisk = false,
}) async {
Options requestOptions = options ?? Options();
requestOptions = requestOptions.copyWith(
extra: {
"refresh": refresh,
"noCache": noCache,
"cacheKey": cacheKey,
"cacheDisk": cacheDisk,
},
);
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
Response response;
response = await dio.get(
path,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future post(
String path, {
Map<String, dynamic>? params,
data,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.post(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future put(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.put(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future patch(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.patch(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
Future delete(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
Options requestOptions = options ?? Options();
Map<String, dynamic>? _authorization = getAuthorizationHeader();
if (_authorization != null) {
requestOptions = requestOptions.copyWith(headers: _authorization);
}
var response = await dio.delete(
path,
data: data,
queryParameters: params,
options: requestOptions,
cancelToken: cancelToken ?? _cancelToken,
);
return response.data;
}
}
dio拦截器
下面我们来看下拦截器,下面是一个处理处理拦截器案例
// 这里是一个我单独写得soket错误实例,因为dio默认生成的是不允许修改message内容的,我只能自定义一个使用
class MyDioSocketException extends SocketException {
late String message;
MyDioSocketException(
message, {
osError,
address,
port,
}) : super(
message,
osError: osError,
address: address,
port: port,
);
}
/// 错误处理拦截器
class ErrorInterceptor extends Interceptor {
// 是否有网
Future<bool> isConnected() async {
var connectivityResult = await (Connectivity().checkConnectivity());
return connectivityResult != ConnectivityResult.none;
}
@override
Future<void> onError(DioError err, ErrorInterceptorHandler errCb) async {
// 自定义一个socket实例,因为dio原生的实例,message属于是只读的
// 这里是我单独加的,因为默认的dio err实例,的几种类型,缺少无网络情况下的错误提示信息
// 这里我手动做处理,来加工一手,效果,看下面的图片,你就知道
if (err.error is SocketException) {
err.error = MyDioSocketException(
err.message,
osError: err.error?.osError,
address: err.error?.address,
port: err.error?.port,
);
}
// dio默认的错误实例,如果是没有网络,只能得到一个未知错误,无法精准的得知是否是无网络的情况
if (err.type == DioErrorType.other) {
bool isConnectNetWork = await isConnected();
if (!isConnectNetWork && err.error is MyDioSocketException) {
err.error.message = "当前网络不可用,请检查您的网络";
}
}
// error统一处理
AppException appException = AppException.create(err);
// 错误提示
debugPrint('DioError===: ${appException.toString()}');
err.error = appException;
return super.onError(err, errCb);
}
}
以上的代码可以看到,ErrorInterceptor类继承自Interceptor,可以重新onRequest 、onResponse、onError,三个状态,最后return super.onError将err实例传递给超类。
统一的错误信息包装处理
试想一下,如果你的项目,有十几种状态码,每种也也都需要吧code码转换成文字信息,因为有时候你需要给用户提示。例如:连接超时,请求失败,网络错误,等等。下面是统一的错误处理
AppException.dart
import 'package:dio/dio.dart';
/// 自定义异常
class AppException implements Exception {
final String _message;
final int _code;
AppException(
this._code,
this._message,
);
String toString() {
return "$_code$_message";
}
String getMessage() {
return _message;
}
factory AppException.create(DioError error) {
switch (error.type) {
case DioErrorType.cancel:
{
return BadRequestException(-1, "请求取消");
}
case DioErrorType.connectTimeout:
{
return BadRequestException(-1, "连接超时");
}
case DioErrorType.sendTimeout:
{
return BadRequestException(-1, "请求超时");
}
case DioErrorType.receiveTimeout:
{
return BadRequestException(-1, "响应超时");
}
case DioErrorType.response:
{
try {
int? errCode = error.response!.statusCode;
// String errMsg = error.response.statusMessage;
// return ErrorEntity(code: errCode, message: errMsg);
switch (errCode) {
case 400:
{
return BadRequestException(errCode!, "请求语法错误");
}
case 401:
{
return UnauthorisedException(errCode!, "没有权限");
}
case 403:
{
return UnauthorisedException(errCode!, "服务器拒绝执行");
}
case 404:
{
return UnauthorisedException(errCode!, "无法连接服务器");
}
case 405:
{
return UnauthorisedException(errCode!, "请求方法被禁止");
}
case 500:
{
return UnauthorisedException(errCode!, "服务器内部错误");
}
case 502:
{
return UnauthorisedException(errCode!, "无效的请求");
}
case 503:
{
return UnauthorisedException(errCode!, "服务器挂了");
}
case 505:
{
return UnauthorisedException(errCode!, "不支持HTTP协议请求");
}
default:
{
// return ErrorEntity(code: errCode, message: "未知错误");
return AppException(errCode!, error.response!.statusMessage!);
}
}
} on Exception catch (_) {
return AppException(-1, "未知错误");
}
}
default:
{
return AppException(-1, error.error.message);
}
}
}
}
/// 请求错误
class BadRequestException extends AppException {
BadRequestException(int code, String message) : super(code, message);
}
/// 未认证异常
class UnauthorisedException extends AppException {
UnauthorisedException(int code, String message) : super(code, message);
}
使用的时候这样使用,
Future<ApiResponse<Feed>> getFeedData(url) async {
try {
dynamic response = await HttpUtils.get(url);
// print(response);
Feed data = Feed.fromJson(response);
return ApiResponse.completed(data);
} on DioError catch (e) {
print(e);
// 这里看这里,如果是有错误的请求下,使用AppException对错误对象进行处理
// 处理过后,你就可以比如弹个toast,提示给用户等,
// 弹窗toast等在下面的方法中调用
return ApiResponse.error(e.error);
}
}
Future<void> _refresh() async {
ApiResponse<Feed> swiperResponse = await getFeedData(initPageUrl);
// 加工过后,我们可以获得两个状态,Status.COMPLETED 和 Status.ERROR
// 看这里
if (swiperResponse.status == Status.COMPLETED) {
// 成功的代码,想干嘛干嘛
}else if (swiperResponse.status == Status.ERROR) {
// 失败的代码,可以给个toast,提示给用户
// 例如我在这里提示用户
// 使用 exception!.getMessage(); 获得错误对象的文字信息,是我们拦截器处理过后的提示文字,非英文,拿到这,提示给用户不香吗???看下面的图片效果
String errMsg = swiperResponse.exception!.getMessage();
publicToast(errMsg);
}
}
这里的提示就是自定义err拦截器中增加的代码,对于dio不能够得到是否无网络的补充
磁盘缓存数据,拦截器
磁盘缓存接口数据,首先我们要封装一个SpUtil类,
sputils.dart
class SpUtil {
SpUtil._internal();
static final SpUtil _instance = SpUtil._internal();
factory SpUtil() {
return _instance;
}
SharedPreferences? prefs;
Future<void> init() async {
prefs = await SharedPreferences.getInstance();
}
Future<bool> setJSON(String key, dynamic jsonVal) {
String jsonString = jsonEncode(jsonVal);
return prefs!.setString(key, jsonString);
}
dynamic getJSON(String key) {
String? jsonString = prefs?.getString(key);
return jsonString == null ? null : jsonDecode(jsonString);
}
Future<bool> setBool(String key, bool val) {
return prefs!.setBool(key, val);
}
bool? getBool(String key) {
return prefs!.getBool(key);
}
Future<bool> remove(String key) {
return prefs!.remove(key);
}
}
缓存拦截器
const int CACHE_MAXAGE = 86400000;
const int CACHE_MAXCOUNT = 1000;
const bool CACHE_ENABLE = false;
class CacheObject {
CacheObject(this.response)
: timeStamp = DateTime.now().millisecondsSinceEpoch;
Response response;
int timeStamp;
@override
bool operator ==(other) {
return response.hashCode == other.hashCode;
}
@override
int get hashCode => response.realUri.hashCode;
}
class NetCacheInterceptor extends Interceptor {
// 为确保迭代器顺序和对象插入时间一致顺序一致,我们使用LinkedHashMap
var cache = LinkedHashMap<String, CacheObject>();
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler requestCb,
) async {
if (!CACHE_ENABLE) {
return super.onRequest(options, requestCb);
}
// refresh标记是否是刷新缓存
bool refresh = options.extra["refresh"] == true;
// 是否磁盘缓存
bool cacheDisk = options.extra["cacheDisk"] == true;
// 如果刷新,先删除相关缓存
if (refresh) {
// 删除uri相同的内存缓存
delete(options.uri.toString());
// 删除磁盘缓存
if (cacheDisk) {
await SpUtil().remove(options.uri.toString());
}
return;
}
// get 请求,开启缓存
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == 'get') {
String key = options.extra["cacheKey"] ?? options.uri.toString();
// 策略 1 内存缓存优先,2 然后才是磁盘缓存
// 1 内存缓存
var ob = cache[key];
if (ob != null) {
//若缓存未过期,则返回缓存内容
if ((DateTime.now().millisecondsSinceEpoch - ob.timeStamp) / 1000 <
CACHE_MAXAGE) {
return;
} else {
//若已过期则删除缓存,继续向服务器请求
cache.remove(key);
}
}
// 2 磁盘缓存
if (cacheDisk) {
var cacheData = SpUtil().getJSON(key);
if (cacheData != null) {
return;
}
}
}
return super.onRequest(options, requestCb);
}
@override
void onResponse(
Response response, ResponseInterceptorHandler responseCb) async {
// 如果启用缓存,将返回结果保存到缓存
if (CACHE_ENABLE) {
await _saveCache(response);
}
return super.onResponse(response, responseCb);
}
Future<void> _saveCache(Response object) async {
RequestOptions options = object.requestOptions;
// 只缓存 get 的请求
if (options.extra["noCache"] != true &&
options.method.toLowerCase() == "get") {
// 策略:内存、磁盘都写缓存
// 缓存key
String key = options.extra["cacheKey"] ?? options.uri.toString();
// 磁盘缓存
if (options.extra["cacheDisk"] == true) {
await SpUtil().setJSON(key, object.data);
}
// 内存缓存
// 如果缓存数量超过最大数量限制,则先移除最早的一条记录
if (cache.length == CACHE_MAXCOUNT) {
cache.remove(cache[cache.keys.first]);
}
cache[key] = CacheObject(object);
}
}
void delete(String key) {
cache.remove(key);
}
}
开始封装
class HttpUtils {
static void init({
required String baseUrl,
int connectTimeout = 1500,
int receiveTimeout = 1500,
List<Interceptor>? interceptors,
}) {
Http().init(
baseUrl: baseUrl,
connectTimeout: connectTimeout,
receiveTimeout: receiveTimeout,
interceptors: interceptors,
);
}
static void cancelRequests({required CancelToken token}) {
Http().cancelRequests(token: token);
}
static Future get(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
bool refresh = false,
bool noCache = !CACHE_ENABLE,
String? cacheKey,
bool cacheDisk = false,
}) async {
return await Http().get(
path,
params: params,
options: options,
cancelToken: cancelToken,
refresh: refresh,
noCache: noCache,
cacheKey: cacheKey,
);
}
static Future post(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().post(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
static Future put(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().put(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
static Future patch(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().patch(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
static Future delete(
String path, {
data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
return await Http().delete(
path,
data: data,
params: params,
options: options,
cancelToken: cancelToken,
);
}
}
注入,初始化
main。dart。这里参考我个人的使用例子
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// debugPaintSizeEnabled = true;
await initStore();
runApp(MyApp());
}
Future<void> initStore() async {
// 初始化本地存储类
await SpUtil().init();
// 初始化request类
HttpUtils.init(
baseUrl: Api.baseUrl,
);
// 历史记录,全局 getx全局注入,
await Get.putAsync(() => HistoryService().init());
print("全局注入");
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetMaterialApp(
debugShowCheckedModeBanner: false,
initialRoute: PageRoutes.INIT_ROUTER,
getPages: PageRoutes.routes,
);
}
}
使用封装好得例子
// 这里定义一个函数,返回的是future 《apiResponse》,可以得到status的状态
Future<ApiResponse<Feed>> getFeedData(url) async {
try {
dynamic response = await HttpUtils.get(url);
// print(response);
Feed data = Feed.fromJson(response);
return ApiResponse.completed(data);
} on DioError catch (e) {
print(e);
return ApiResponse.error(e.error);
}
}
Future<void> _refresh() async {
ApiResponse<Feed> swiperResponse = await getFeedData(initPageUrl);
if (!mounted) {
return;
}
// 使用 status.COMPLETED 判断是否成功
if (swiperResponse.status == Status.COMPLETED) {
setState(() {
nextPageUrl = swiperResponse.data!.nextPageUrl;
_swiperList = [];
_swiperList.addAll(swiperResponse.data!.issueList![0]!.itemList!);
_itemList = [];
});
// 拉取新的,列表
await _loading();
// 使用 status.ERROR 判断是否失败
} else if (swiperResponse.status == Status.ERROR) {
setState(() {
stateCode = 2;
});
// 错误的话,我们可以调用 getMessage() 获取错误信息。提示给用户(汉化后的友好提示语)
String errMsg = swiperResponse.exception!.getMessage();
publicToast(errMsg);
print("发生错误,位置home bottomBar1 swiper, url: ${initPageUrl}");
print(swiperResponse.exception);
}
}
如果要白嫖完整的方案
可以参考使用这套方案开发的 flutter + getx 仿开眼视频app,有star的大佬可以赏点star。
项目地址 https://github.com/abcd498936590/flutter_eyepetizer
apk下载 https://pan.baidu.com/share/init?surl=5T13BzbwG_NI8pbaVISoDA
提取码:3ev2