在 Flutter 中,Dio 是最常用的网络请求库,合理的封装可以提升代码复用性、降低维护成本,还能统一处理请求拦截、错误处理、超时配置等通用逻辑。以下是一套标准化的 Dio 封装方案,包含核心封装、拦截器配置、业务适配和使用示例。
一、核心封装思路
- 单例模式:避免重复创建 Dio 实例,统一管理配置;
- 拦截器:统一处理请求头、请求 / 响应日志、token 刷新、错误处理;
- 通用方法封装:封装 GET/POST/PUT/DELETE 等基础请求,简化业务层调用;
- 数据解析:统一处理接口返回格式,剥离业务数据与通用包装;
- 异常处理:封装自定义异常,区分网络错误、业务错误、超时错误等。
二、完整封装实现
步骤 1:添加依赖
在 pubspec.yaml 中添加 Dio 依赖(建议指定稳定版本):
yaml
yaml
dependencies:
dio: ^5.4.3+1 # 最新版本可查看 pub.dev
flutter_dotenv: ^5.1.0 # 可选,用于管理环境变量(如 baseUrl)
cookie_jar: ^4.0.1 # 可选,用于 cookie 管理
步骤 2:创建网络配置文件
新建 lib/core/net/network_config.dart,管理基础配置:
dart
dart
import 'package:flutter_dotenv/flutter_dotenv.dart';
/// 网络请求配置
class NetworkConfig {
/// 基础域名(可通过环境变量区分开发/生产环境)
static String baseUrl = dotenv.env['BASE_URL'] ?? 'https://api.example.com';
/// 请求超时时间(毫秒)
static const int connectTimeout = 10000;
/// 响应超时时间(毫秒)
static const int receiveTimeout = 10000;
/// 默认请求头
static Map<String, String> defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
步骤 3:封装 Dio 实例(核心)
新建 lib/core/net/dio_client.dart,实现 Dio 单例和拦截器:
dart
dart
import 'dart:convert';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'network_config.dart';
import 'network_exception.dart'; // 后续创建自定义异常
/// Dio 单例客户端
class DioClient {
static DioClient? _instance;
late Dio _dio;
/// 私有构造方法
DioClient._internal() {
// 初始化 Dio
_dio = Dio(
BaseOptions(
baseUrl: NetworkConfig.baseUrl,
connectTimeout: Duration(milliseconds: NetworkConfig.connectTimeout),
receiveTimeout: Duration(milliseconds: NetworkConfig.receiveTimeout),
headers: NetworkConfig.defaultHeaders,
responseType: ResponseType.json,
),
);
// 添加拦截器
_addInterceptors();
}
/// 获取单例
static DioClient get instance {
_instance ??= DioClient._internal();
return _instance!;
}
/// 获取原始 Dio 实例(特殊场景使用)
Dio get dio => _dio;
/// 添加拦截器
void _addInterceptors() {
// 1. Cookie 拦截器(可选,根据业务需求)
_dio.interceptors.add(CookieManager(CookieJar()));
// 2. 日志拦截器(开发环境开启,生产环境关闭)
_dio.interceptors.add(
LogInterceptor(
request: true, // 打印请求信息
requestHeader: true, // 打印请求头
requestBody: true, // 打印请求体
responseHeader: true, // 打印响应头
responseBody: true, // 打印响应体
error: true, // 打印错误信息
),
);
// 3. 自定义拦截器(处理 token、请求头、响应统一解析等)
_dio.interceptors.add(
InterceptorsWrapper(
// 请求拦截
onRequest: (RequestOptions options, RequestInterceptorHandler handler) async {
// 示例:添加 token 到请求头
String? token = _getToken(); // 从本地缓存获取 token
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options); // 继续请求
},
// 响应拦截
onResponse: (Response response, ResponseInterceptorHandler handler) {
// 统一解析响应数据(假设接口返回格式:{code: 200, msg: "成功", data: {...}})
Map<String, dynamic> data = response.data;
int code = data['code'] ?? -1;
String msg = data['msg'] ?? '请求失败';
if (code == 200) {
// 业务成功,只返回 data 给上层
response.data = data['data'];
handler.next(response);
} else {
// 业务错误,抛出自定义异常
handler.reject(
DioException(
requestOptions: response.requestOptions,
error: NetworkException(code: code, msg: msg),
),
);
}
},
// 错误拦截
onError: (DioException e, ErrorInterceptorHandler handler) {
// 统一处理错误(网络错误、超时、业务错误等)
NetworkException exception = _convertDioErrorToNetworkException(e);
handler.reject(
DioException(
requestOptions: e.requestOptions,
error: exception,
type: e.type,
message: exception.msg,
),
);
},
),
);
}
/// 从本地缓存获取 token(示例方法,需根据实际实现)
String? _getToken() {
// 示例:从 SharedPreferences 获取
// SharedPreferences prefs = await SharedPreferences.getInstance();
// return prefs.getString('token');
return null;
}
/// 将 Dio 错误转换为自定义网络异常
NetworkException _convertDioErrorToNetworkException(DioException e) {
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.sendTimeout) {
return NetworkException(code: -1, msg: '请求超时,请检查网络');
} else if (e.type == DioExceptionType.connectionError) {
return NetworkException(code: -2, msg: '网络连接失败,请检查网络');
} else if (e.type == DioExceptionType.badResponse) {
// HTTP 状态码错误(404、500 等)
int statusCode = e.response?.statusCode ?? -3;
return NetworkException(code: statusCode, msg: '服务器错误($statusCode)');
} else if (e.error is NetworkException) {
// 业务错误(已在响应拦截器抛出)
return e.error as NetworkException;
} else {
return NetworkException(code: -999, msg: '未知错误:${e.message}');
}
}
// ---------------------- 通用请求方法封装 ----------------------
/// GET 请求
Future<T> get<T>(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
try {
Response response = await _dio.get(
path,
queryParameters: params,
options: options,
cancelToken: cancelToken,
);
return response.data as T;
} on DioException catch (e) {
throw e.error as NetworkException;
}
}
/// POST 请求
Future<T> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
try {
Response response = await _dio.post(
path,
data: data,
queryParameters: params,
options: options,
cancelToken: cancelToken,
);
return response.data as T;
} on DioException catch (e) {
throw e.error as NetworkException;
}
}
/// PUT 请求
Future<T> put<T>(
String path, {
dynamic data,
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
try {
Response response = await _dio.put(
path,
data: data,
queryParameters: params,
options: options,
cancelToken: cancelToken,
);
return response.data as T;
} on DioException catch (e) {
throw e.error as NetworkException;
}
}
/// DELETE 请求
Future<T> delete<T>(
String path, {
Map<String, dynamic>? params,
Options? options,
CancelToken? cancelToken,
}) async {
try {
Response response = await _dio.delete(
path,
queryParameters: params,
options: options,
cancelToken: cancelToken,
);
return response.data as T;
} on DioException catch (e) {
throw e.error as NetworkException;
}
}
/// 上传文件
Future<T> upload<T>(
String path, {
required MultipartFile file,
String fileName = 'file',
Map<String, dynamic>? params,
CancelToken? cancelToken,
ProgressCallback? onSendProgress,
}) async {
try {
FormData formData = FormData.fromMap({
fileName: file,
if (params != null) ...params,
});
Response response = await _dio.post(
path,
data: formData,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
options: Options(
contentType: 'multipart/form-data',
),
);
return response.data as T;
} on DioException catch (e) {
throw e.error as NetworkException;
}
}
}
步骤 4:封装自定义网络异常
新建 lib/core/net/network_exception.dart,统一异常类型:
dart
/// 自定义网络异常(区分业务错误和系统错误)
class NetworkException implements Exception {
final int code; // 错误码(业务码/HTTP状态码/系统码)
final String msg; // 错误信息
NetworkException({required this.code, required this.msg});
@override
String toString() => 'NetworkException(code: $code, msg: $msg)';
}
步骤 5:业务层 API 封装(示例)
新建 lib/api/user_api.dart,封装具体业务接口:
dart
dart
import 'package:your_project/core/net/dio_client.dart';
import 'package:your_project/core/net/network_exception.dart';
/// 用户相关 API
class UserApi {
static final DioClient _dioClient = DioClient.instance;
/// 登录接口
static Future<Map<String, dynamic>> login({
required String username,
required String password,
}) async {
try {
return await _dioClient.post(
'/user/login',
data: {
'username': username,
'password': password,
},
);
} on NetworkException catch (e) {
// 可在此处针对特定错误码做处理(如登录失败、账号冻结等)
if (e.code == 401) {
throw NetworkException(code: 401, msg: '用户名或密码错误');
}
rethrow; // 其他错误抛给上层处理
}
}
/// 获取用户信息
static Future<Map<String, dynamic>> getUserInfo() async {
return await _dioClient.get('/user/info');
}
}
三、在页面中使用
dart
php
import 'package:flutter/material.dart';
import 'package:your_project/api/user_api.dart';
import 'package:your_project/core/net/network_exception.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
final TextEditingController _usernameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
/// 登录按钮点击事件
Future<void> _login() async {
String username = _usernameController.text.trim();
String password = _passwordController.text.trim();
if (username.isEmpty || password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请输入用户名和密码')),
);
return;
}
try {
// 显示加载中
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => const Center(child: CircularProgressIndicator()),
);
// 调用登录接口
Map<String, dynamic> userData = await UserApi.login(
username: username,
password: password,
);
// 登录成功,处理逻辑(如保存 token、跳转到首页)
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('登录成功')),
);
Navigator.pop(context); // 关闭加载弹窗
// Navigator.pushReplacementNamed(context, '/home');
} on NetworkException catch (e) {
// 处理错误
Navigator.pop(context); // 关闭加载弹窗
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.msg)),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('登录')),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
TextField(
controller: _usernameController,
decoration: const InputDecoration(hintText: '用户名'),
),
const SizedBox(height: 16),
TextField(
controller: _passwordController,
obscureText: true,
decoration: const InputDecoration(hintText: '密码'),
),
const SizedBox(height: 32),
ElevatedButton(
onPressed: _login,
child: const Text('登录'),
),
],
),
),
);
}
}
四、进阶优化点
- 环境切换 :通过
flutter_dotenv管理开发 / 测试 / 生产环境的baseUrl; - Token 刷新:在请求拦截器中检测 token 过期,自动刷新 token 并重试请求;
- 取消请求 :使用
CancelToken处理页面销毁时取消未完成的请求,避免内存泄漏; - 缓存策略 :结合
dio_cache_interceptor实现 GET 请求缓存; - 重试机制:添加重试拦截器,针对网络波动自动重试请求;
- SSL 证书校验:对接后台 HTTPS 证书,防止抓包(生产环境必备);
- 请求加密:对敏感请求(如登录)进行参数加密 / 签名。
五、核心优势
- 统一管理:所有网络配置、拦截器集中管理,修改方便;
- 简化调用:业务层只需关注接口参数和返回值,无需处理通用逻辑;
- 错误统一 :所有错误转换为自定义异常,上层只需捕获
NetworkException; - 可扩展性:新增拦截器、修改配置不影响业务代码。
这套封装方案适配大多数 Flutter 项目的网络需求,可根据实际业务调整(如接口返回格式、token 逻辑、异常类型等)。