导言:
在现代移动应用开发中,与服务器进行HTTP网络通信是必不可少的一环。Flutter提供了基础的 http 包,但功能相对简单。Dio 是一个强大且易用的Dart/Flutter HTTP客户端,它支持拦截器、全局配置、FormData、请求取消、文件上传/下载、超时等高级功能。本讲将带你从零开始,全面掌握Dio的使用,并学会如何对其进行企业级封装。
一、 为什么选择Dio?
在开始之前,我们简单对比一下原生 http 包和 dio:
| 特性 | Dart http 包 |
Dio |
|---|---|---|
| 易用性 | 基础,需要手动解析JSON | API友好,支持多种数据转换 |
| 拦截器 | 不支持 | 强大支持,可全局处理请求/响应/错误 |
| 文件操作 | 需要自己处理流 | 原生支持文件上传/下载,进度回调 |
| 请求取消 | 不支持 | 支持 |
| 超时配置 | 全局配置 | 可针对每个请求配置 |
| 自动重试 | 需手动实现 | 通过拦截器可轻松实现 |
结论:对于大多数严肃的商业项目,Dio是不二之选。
二、 基础使用:快速上手
1. 添加依赖
在 pubspec.yaml 文件中添加依赖:
yaml
复制
下载
dependencies:
dio: ^5.0.0 # 请使用最新版本
然后在终端运行 flutter pub get。
2. 发起一个GET请求
dart
复制
下载
import 'package:dio/dio.dart';
void fetchUserData() async {
try {
// 创建一个Dio实例
final dio = Dio();
// 发起GET请求
final response = await dio.get('https://jsonplaceholder.typicode.com/users/1');
// 请求成功,response.data 已经是解析好的Map或List
print(response.data); // 直接是Map类型,无需手动jsonDecode
print(response.statusCode); // HTTP状态码
} on DioException catch (e) {
// 使用 DioException 捕获 Dio 相关的错误
// 这是 Dio 5.x 的变更,之前是 DioError
print('请求出错: ${e.message}');
if (e.response != null) {
// 服务器返回了错误状态码(如404, 500等)
print('服务器错误: ${e.response?.statusCode}');
print('服务器错误信息: ${e.response?.data}');
}
} catch (e) {
// 处理其他异常(如网络连接失败)
print('其他错误: $e');
}
}
3. 发起一个POST请求
dart
复制
下载
void postUserData() async {
final dio = Dio();
try {
final response = await dio.post(
'https://jsonplaceholder.typicode.com/posts',
data: {
'title': 'foo',
'body': 'bar',
'userId': 1,
}, // Dio会自动将Map编码为JSON
options: Options(
headers: {
'Content-Type': 'application/json; charset=UTF-8',
},
),
);
print('创建成功: ${response.data}');
} on DioException catch (e) {
print('提交失败: ${e.message}');
}
}
三、 核心配置与全局初始化
在实际项目中,我们通常会创建一个全局的、配置好的Dio实例。
dart
复制
下载
// network/dio_client.dart
class DioClient {
// 静态实例
static final DioClient _instance = DioClient._internal();
// 工厂构造函数,返回单例
factory DioClient() {
return _instance;
}
// 内部的Dio实例
final Dio dio;
// 私有构造函数,在这里进行初始化
DioClient._internal()
: dio = Dio(
BaseOptions(
// 基础URL,后续所有请求会自动拼接
baseUrl: 'https://jsonplaceholder.typicode.com',
// 连接超时时间(毫秒)
connectTimeout: const Duration(seconds: 10),
// 接收超时时间(毫秒)
receiveTimeout: const Duration(seconds: 10),
// 发送超时时间(毫秒)
sendTimeout: const Duration(seconds: 10),
// 请求头
headers: {
'Content-Type': 'application/json',
},
),
) {
// 可以在这里添加拦截器
_addInterceptors();
}
void _addInterceptors() {
// 添加日志拦截器(需要 dio_logger 包)
// dio.interceptors.add(LogInterceptor());
// 添加自定义拦截器
dio.interceptors.add(CustomInterceptor());
}
}
四、 拦截器(Interceptors):Dio的灵魂
拦截器允许你在请求发出前或响应返回后,对其进行统一处理。
1. 自定义拦截器示例
dart
复制
下载
// interceptors/custom_interceptor.dart
class CustomInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('🚀 发出请求: ${options.method} ${options.uri}');
// 在实际项目中,可以在这里统一添加Token
// options.headers['Authorization'] = 'Bearer $token';
// 必须调用 handler.next 继续执行
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
print('✅ 收到响应: ${response.statusCode} ${response.requestOptions.uri}');
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
print('❌ 请求错误: ${err.type} - ${err.message}');
// 统一错误处理
_handleError(err);
handler.next(err);
}
void _handleError(DioException err) {
switch (err.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
// 显示超时错误提示
break;
case DioExceptionType.badResponse:
// 处理服务器返回的错误状态码
final statusCode = err.response?.statusCode;
if (statusCode == 401) {
// Token过期,跳转到登录页
// _goToLogin();
}
break;
case DioExceptionType.cancel:
// 请求被取消
break;
case DioExceptionType.unknown:
// 其他错误,通常是网络问题
break;
default:
break;
}
}
}
2. 使用拦截器实现自动Token刷新
这是一个高级但非常实用的场景:
dart
复制
下载
class TokenRefreshInterceptor extends Interceptor {
final Dio _dio;
final Future<String> Function() _refreshToken;
TokenRefreshInterceptor(this._dio, this._refreshToken);
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// 如果是401错误且不是刷新Token的请求本身
if (err.response?.statusCode == 401 &&
!err.requestOptions.path.contains('/refresh-token')) {
try {
// 尝试刷新Token
final newToken = await _refreshToken();
// 更新请求头中的Token
err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
// 重新发起失败的请求
final response = await _dio.fetch(err.requestOptions);
handler.resolve(response); // 返回成功的结果
} catch (e) {
// 刷新Token失败,跳转到登录页
// _goToLogin();
handler.reject(err); // 继续抛出错误
}
} else {
handler.next(err);
}
}
}
五、 企业级封装:ApiService模式
为了更好的维护性和可测试性,我们通常会对网络请求进行进一步封装。
1. 定义API端点
dart
复制
下载
// api/endpoints.dart
abstract class ApiEndpoints {
static const String baseUrl = 'https://jsonplaceholder.typicode.com';
static const String getUser = '/users/{id}';
static const String createPost = '/posts';
static const String uploadImage = '/upload';
}
2. 创建ApiService
dart
复制
下载
// services/api_service.dart
class ApiService {
final Dio _dio;
ApiService(this._dio);
// 获取用户信息
Future<User> getUser(int id) async {
final response = await _dio.get(
ApiEndpoints.getUser.replaceFirst('{id}', id.toString()),
);
return User.fromJson(response.data);
}
// 创建帖子
Future<Post> createPost(Post post) async {
final response = await _dio.post(
ApiEndpoints.createPost,
data: post.toJson(),
);
return Post.fromJson(response.data);
}
// 文件上传
Future<String> uploadImage(String filePath) async {
// 创建FormData
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath),
});
final response = await _dio.post(
ApiEndpoints.uploadImage,
data: formData,
// 上传进度回调
onSendProgress: (sent, total) {
if (total != -1) {
print('上传进度: ${(sent / total * 100).toStringAsFixed(0)}%');
}
},
);
return response.data['url']; // 假设返回图片URL
}
}
3. 在项目中使用的完整示例
dart
复制
下载
// main.dart 或依赖注入的设置
void main() {
// 初始化Dio客户端
final dioClient = DioClient();
// 创建ApiService
final apiService = ApiService(dioClient.dio);
runApp(MyApp(apiService: apiService));
}
class MyApp extends StatelessWidget {
final ApiService apiService;
const MyApp({super.key, required this.apiService});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: UserProfilePage(apiService: apiService),
);
}
}
// 使用ApiService的页面
class UserProfilePage extends StatefulWidget {
final ApiService apiService;
const UserProfilePage({super.key, required this.apiService});
@override
State<UserProfilePage> createState() => _UserProfilePageState();
}
class _UserProfilePageState extends State<UserProfilePage> {
User? _user;
bool _isLoading = false;
@override
void initState() {
super.initState();
_fetchUserData();
}
Future<void> _fetchUserData() async {
setState(() {
_isLoading = true;
});
try {
final user = await widget.apiService.getUser(1);
setState(() {
_user = user;
});
} on DioException catch (e) {
// 显示错误提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('加载失败: ${e.message}')),
);
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('用户信息')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _user != null
? UserInfoWidget(user: _user!)
: const Center(child: Text('加载失败')),
);
}
}
六、 错误处理的统一规范
为了在整个应用中保持一致的错误处理体验,建议创建统一的错误处理工具:
dart
复制
下载
// utils/error_handler.dart
class ErrorHandler {
static String getErrorMessage(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return '网络连接超时,请检查网络后重试';
case DioExceptionType.badResponse:
switch (e.response?.statusCode) {
case 401:
return '登录已过期,请重新登录';
case 404:
return '请求的资源不存在';
case 500:
case 502:
case 503:
return '服务器开小差了,请稍后重试';
default:
return '网络请求失败: ${e.response?.statusCode}';
}
case DioExceptionType.cancel:
return '请求已取消';
case DioExceptionType.unknown:
return '网络连接失败,请检查网络设置';
default:
return '未知错误,请稍后重试';
}
}
}
七、 总结与最佳实践
通过本讲的学习,你应该已经掌握了:
-
Dio的基础使用:GET/POST请求,错误处理。
-
全局配置:创建配置良好的单例Dio客户端。
-
拦截器:日志记录、统一错误处理、Token自动刷新。
-
企业级封装:ApiService模式,职责分离。
-
文件上传:使用FormData进行文件操作。