第14讲:HTTP网络请求 - Dio库的使用与封装
导言:
在现代移动应用开发中,与服务器进行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或Listprint(response.data); // 直接是Map类型,无需手动jsonDecodeprint(response.statusCode); // HTTP状态码} on DioException catch (e) {// 使用 DioException 捕获 Dio 相关的错误// 这是 Dio 5.x 的变更,之前是 DioErrorprint('请求出错: ${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编码为JSONoptions: Options(headers: {'Content-Type': 'application/json; charset=UTF-8',},),);print('创建成功: ${response.data}');} on DioException catch (e) {print('提交失败: ${e.message}');}
}三、 核心配置与全局初始化
在实际项目中,我们通常会创建一个全局的、配置好的Dio实例。
dart
复制
下载
// network/dio_client.dartclass 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.dartclass CustomInterceptor extends Interceptor {@overridevoid onRequest(RequestOptions options, RequestInterceptorHandler handler) {print('🚀 发出请求: ${options.method} ${options.uri}');// 在实际项目中,可以在这里统一添加Token// options.headers['Authorization'] = 'Bearer $token';// 必须调用 handler.next 继续执行handler.next(options);}@overridevoid onResponse(Response response, ResponseInterceptorHandler handler) {print('✅ 收到响应: ${response.statusCode} ${response.requestOptions.uri}');handler.next(response);}@overridevoid 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);@overridevoid onError(DioException err, ErrorInterceptorHandler handler) async {// 如果是401错误且不是刷新Token的请求本身if (err.response?.statusCode == 401 && !err.requestOptions.path.contains('/refresh-token')) {try {// 尝试刷新Tokenfinal newToken = await _refreshToken();// 更新请求头中的Tokenerr.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.dartabstract 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.dartclass 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 {// 创建FormDatafinal 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();// 创建ApiServicefinal apiService = ApiService(dioClient.dio);runApp(MyApp(apiService: apiService));
}class MyApp extends StatelessWidget {final ApiService apiService;const MyApp({super.key, required this.apiService});@overrideWidget build(BuildContext context) {return MaterialApp(home: UserProfilePage(apiService: apiService),);}
}// 使用ApiService的页面
class UserProfilePage extends StatefulWidget {final ApiService apiService;const UserProfilePage({super.key, required this.apiService});@overrideState<UserProfilePage> createState() => _UserProfilePageState();
}class _UserProfilePageState extends State<UserProfilePage> {User? _user;bool _isLoading = false;@overridevoid 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;});}}@overrideWidget 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.dartclass 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进行文件操作。
