常用第三方库:dio网络库使用与封装
前言
dio是Flutter生态中最受欢迎的网络请求库之一,它提供了强大的功能和灵活的配置选项。本文将从实战角度深入介绍dio的使用技巧和最佳实践。
基础知识
1. dio简介
dio是一个强大的Dart Http请求库,支持Restful API、FormData、拦截器、请求取消、Cookie管理、文件上传/下载、超时等特性。
2. 基本使用
// 安装依赖
// pubspec.yaml
dependencies:
dio: ^5.3.2
// 基本使用示例
import 'package:dio/dio.dart';
final dio = Dio();
// GET请求
Response response = await dio.get('https://api.example.com/users');
// POST请求
Response response = await dio.post(
'https://api.example.com/users',
data: {'name': 'test', 'age': 25},
);
3. 配置选项
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
headers: {
'Authorization': 'Bearer token',
'Content-Type': 'application/json',
},
));
进阶特性
1. 拦截器
拦截器是dio的一大特色,可以在请求发起前和响应返回后进行统一处理。
class CustomInterceptor extends Interceptor {
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 在请求发起前统一添加token
options.headers['Authorization'] = 'Bearer ${getToken()}';
handler.next(options);
}
void onResponse(Response response, ResponseInterceptorHandler handler) {
// 统一处理响应数据
if (response.data['code'] == 0) {
handler.next(response);
} else {
handler.reject(DioException(
requestOptions: response.requestOptions,
response: response,
error: response.data['message'],
));
}
}
void onError(DioException err, ErrorInterceptorHandler handler) {
// 统一错误处理
if (err.response?.statusCode == 401) {
// 处理token过期
refreshToken().then((newToken) {
// 重试请求
dio.fetch(err.requestOptions);
});
}
handler.next(err);
}
}
// 添加拦截器
dio.interceptors.add(CustomInterceptor());
2. 请求取消
// 创建取消令牌
final cancelToken = CancelToken();
// 发起请求
dio.get('https://api.example.com/data',
cancelToken: cancelToken).then((response) {
print(response.data);
}).catchError((error) {
if (CancelToken.isCancel(error)) {
print('请求被取消');
}
});
// 取消请求
cancelToken.cancel('用户取消');
3. 文件上传下载
// 文件上传
FormData formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
'./text.txt',
filename: 'text.txt',
),
});
Response response = await dio.post(
'/upload',
data: formData,
onSendProgress: (int sent, int total) {
print('上传进度:${sent / total}');
},
);
// 文件下载
Response response = await dio.download(
'https://example.com/file.pdf',
'./file.pdf',
onReceiveProgress: (int received, int total) {
print('下载进度:${received / total}');
},
);
实战案例:网络请求封装
1. 统一的网络请求类
class HttpClient {
static final HttpClient _instance = HttpClient._internal();
late Dio dio;
factory HttpClient() => _instance;
HttpClient._internal() {
dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
));
// 添加拦截器
dio.interceptors.add(CustomInterceptor());
// 添加缓存拦截器
dio.interceptors.add(CacheInterceptor());
// 添加重试拦截器
dio.interceptors.add(
RetryInterceptor(
dio: dio,
retries: 3,
retryDelays: const [
Duration(seconds: 1),
Duration(seconds: 2),
Duration(seconds: 3),
],
),
);
}
Future<T> get<T>(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
try {
final response = await dio.get(
path,
queryParameters: params,
options: options,
cancelToken: cancelToken,
);
return _handleResponse<T>(response);
} catch (e) {
return _handleError(e);
}
}
Future<T> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
try {
final response = await dio.post(
path,
data: data,
queryParameters: params,
options: options,
cancelToken: cancelToken,
);
return _handleResponse<T>(response);
} catch (e) {
return _handleError(e);
}
}
T _handleResponse<T>(Response response) {
if (response.data['code'] == 0) {
return response.data['data'];
} else {
throw ApiException(
code: response.data['code'],
message: response.data['message'],
);
}
}
Never _handleError(dynamic error) {
if (error is DioException) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
throw const ApiException(message: '连接超时');
case DioExceptionType.sendTimeout:
throw const ApiException(message: '请求超时');
case DioExceptionType.receiveTimeout:
throw const ApiException(message: '响应超时');
case DioExceptionType.badResponse:
throw ApiException(
code: error.response?.statusCode,
message: error.response?.statusMessage,
);
default:
throw ApiException(message: error.message);
}
}
throw ApiException(message: error.toString());
}
}
2. 缓存拦截器实现
class CacheInterceptor extends Interceptor {
final _cache = <String, Response>{};
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// 只缓存GET请求
if (options.method == 'GET') {
final key = options.uri.toString();
final cachedResponse = _cache[key];
if (cachedResponse != null) {
return handler.resolve(cachedResponse);
}
}
handler.next(options);
}
void onResponse(Response response, ResponseInterceptorHandler handler) {
// 缓存响应数据
if (response.requestOptions.method == 'GET') {
final key = response.requestOptions.uri.toString();
_cache[key] = response;
}
handler.next(response);
}
}
3. 使用示例
// API服务类
class UserService {
final _http = HttpClient();
Future<User> getUserInfo(String userId) async {
final response = await _http.get<Map<String, dynamic>>(
'/users/$userId',
);
return User.fromJson(response);
}
Future<List<User>> getUsers({int page = 1, int size = 20}) async {
final response = await _http.get<List<dynamic>>(
'/users',
params: {'page': page, 'size': size},
);
return response.map((json) => User.fromJson(json)).toList();
}
Future<void> updateUser(String userId, Map<String, dynamic> data) async {
await _http.post<void>(
'/users/$userId',
data: data,
);
}
}
性能优化建议
-
合理使用拦截器
- 避免在拦截器中进行耗时操作
- 使用异步操作时注意处理异常
-
请求优化
- 合理设置超时时间
- 使用cancelToken取消不必要的请求
- 避免频繁的重复请求
-
缓存策略
- 针对不常变化的数据实现缓存
- 设置合理的缓存过期时间
- 考虑使用本地存储持久化缓存
-
错误处理
- 实现统一的错误处理机制
- 合理使用重试机制
- 提供友好的错误提示
常见问题解决
- 证书验证问题
// 忽略证书验证
dio.options.validateStatus = (status) {
return status! < 500;
};
// 或者自定义证书验证
(dio.httpClientAdapter as IOHttpClientAdapter).onHttpClientCreate =
(HttpClient client) {
client.badCertificateCallback =
(X509Certificate cert, String host, int port) => true;
return client;
};
- 请求取消后的内存泄漏
// 在dispose时取消所有请求
final _cancelTokens = <CancelToken>[];
void addCancelToken(CancelToken token) {
_cancelTokens.add(token);
}
void dispose() {
for (final token in _cancelTokens) {
token.cancel();
}
_cancelTokens.clear();
super.dispose();
}
面试题解析
1. dio与http package的区别是什么?
答:dio相比http package有以下优势:
- 更强大的功能:支持拦截器、请求取消、文件上传下载进度监听等
- 更好的扩展性:可以通过拦截器机制实现各种自定义功能
- 更完善的错误处理:提供了统一的错误处理机制
- 更方便的配置:支持全局配置和请求级配置
- 更好的性能:支持请求队列、连接池等优化
2. 如何实现token失效自动刷新?
答:可以通过拦截器实现:
- 在响应拦截器中检测token失效(如401状态码)
- 调用刷新token的接口获取新token
- 使用新token重试原请求
- 注意处理并发请求的情况,避免多次刷新token
class TokenInterceptor extends Interceptor {
bool _isRefreshing = false;
List<RequestOptions> _pendingRequests = [];
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
final options = err.requestOptions;
if (!_isRefreshing) {
_isRefreshing = true;
try {
final newToken = await refreshToken();
// 更新所有等待的请求
for (final request in _pendingRequests) {
request.headers['Authorization'] = 'Bearer $newToken';
dio.fetch(request);
}
_pendingRequests.clear();
} finally {
_isRefreshing = false;
}
} else {
_pendingRequests.add(options);
}
return;
}
handler.next(err);
}
}
3. dio如何处理大文件上传?
答:处理大文件上传需要注意以下几点:
- 使用FormData和MultipartFile处理文件上传
- 监听上传进度
- 实现断点续传
- 处理超时和错误重试
Future<void> uploadLargeFile(String filePath) async {
final file = File(filePath);
final fileSize = await file.length();
// 创建FormData
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
filePath,
filename: basename(filePath),
),
});
try {
await dio.post(
'/upload',
data: formData,
options: Options(
headers: {
Headers.contentLengthHeader: fileSize,
},
sendTimeout: Duration(minutes: 10),
),
onSendProgress: (int sent, int total) {
final progress = sent / total;
print('上传进度:${(progress * 100).toStringAsFixed(2)}%');
},
);
} catch (e) {
// 处理错误,实现断点续传逻辑
}
}
4. 如何优化dio的性能?
答:可以从以下几个方面优化dio的性能:
-
连接池优化
- 合理设置maxConnectionsPerHost
- 复用连接
-
请求优化
- 使用适当的超时设置
- 实现请求合并
- 避免频繁创建dio实例
-
响应优化
- 实现数据缓存
- 使用压缩传输
- 合理处理大量数据
-
内存优化
- 及时释放不需要的资源
- 避免内存泄漏
// 示例:请求合并优化
class RequestMerger {
static final _instance = RequestMerger._();
final _pending = <String, Completer<dynamic>>{};
factory RequestMerger() => _instance;
RequestMerger._();
Future<T> request<T>(String url, Future<T> Function() fetcher) async {
if (_pending.containsKey(url)) {
return _pending[url]!.future as Future<T>;
}
final completer = Completer<T>();
_pending[url] = completer;
try {
final result = await fetcher();
completer.complete(result);
return result;
} catch (e) {
completer.completeError(e);
rethrow;
} finally {
_pending.remove(url);
}
}
}
总结
dio是一个功能强大且灵活的HTTP客户端库,通过合理的封装和优化,可以构建出一个健壮的网络请求层。本文介绍了dio的基本使用、进阶特性、实战案例和性能优化建议,希望能帮助读者更好地使用dio进行网络开发。
参考资源
- dio官方文档:https://pub.dev/packages/dio
- Flutter网络请求最佳实践:https://flutter.dev/docs/development/data-and-backend/networking
- dio源码:https://github.com/flutterchina/dio
如果你对文章内容有任何疑问或建议,欢迎在评论区留言交流。