Dio 是什么
Dio 是一个 基于 Dart 的强大 HTTP 客户端,用于发起网络请求(GET、POST 等)和处理响应。
相比原生 http
包,Dio 更灵活,功能更强大,特别适合在 Flutter App 中做 API 调用。
Dio 的主要特点
- 支持拦截器(Interceptor):可以在请求/响应前做统一处理,例如添加 token、打印日志、统一错误处理。
- 支持全局配置:如
baseUrl
、请求超时、headers 等。 - 支持请求取消:可以通过
CancelToken
取消请求,非常适合页面销毁时取消未完成请求。 - 支持 FormData 和文件上传:可以直接上传文件、表单数据,很方便。
- 支持请求重试和拦截错误:可以根据响应状态码统一处理错误。
- 支持 JSON 自动解析:返回数据可以直接解析成
Map
或List
。
Dio 的使用
安装
dependencies:
dio: ^5.9.0
创建 Dio 实例
import 'package:dio/dio.dart';
final dio = Dio(
BaseOptions(
baseUrl: "https://jsonplaceholder.typicode.com",
connectTimeout: 5000, // 连接超时 5 秒
receiveTimeout: 3000, // 接收超时 3 秒
),
);
BaseOptions 常用属性
属性 | 说明 |
---|---|
baseUrl |
基础 URL,可在请求中省略重复的域名 |
connectTimeout |
连接超时时间(毫秒) |
receiveTimeout |
响应超时时间(毫秒) |
headers |
全局请求头 |
responseType |
响应类型,json 、plain 、stream 、bytes |
contentType |
请求内容类型,常用 "application/json" |
followRedirects |
是否跟随重定向 |
validateStatus |
自定义状态码验证,例如只接收 200~299 |
发送请求
// 发送get请求
Response response = await dio.get("/path",
// Map<String, dynamic>? queryParameters,
queryParameters: {"id": 123},
options: Options(headers: {"Authorization": "Bearer TOKEN"}),
);
// POST 请求
Response response = await dio.post("/path",
data: {"title": "Hello", "body": "World"},
options: Options(contentType: Headers.jsonContentType),
);
// PUT / PATCH / DELETE
await dio.put("/path", data: {...});
await dio.patch("/path", data: {...});
await dio.delete("/path", data: {...});
在 Dio 中,data
参数的类型是 Object?
,也就是说你可以传入:①Map(最常用)②List(数组)③String(通常是 JSON 字符串)④其他可序列化对象(比如自定义对象,只要你先转成 JSON)
如果传
String
,最好设置contentType: Headers.jsonContentType
,否则服务器可能无法识别为 JSON。await dio.post("/raw", data: '{"title":"Hello"}', options: Options(contentType: Headers.jsonContentType));
Map 或 List 会被 Dio 自动序列化成 JSON(默认
application/json
)。
Options 常用属性
属性 | 说明 |
---|---|
headers |
请求单独设置 headers |
responseType |
请求单独设置响应类型 |
contentType |
单次请求设置 Content-Type |
extra |
自定义额外参数,可在拦截器中使用 |
sendTimeout |
发送数据超时时间 |
Options 和 BaseOptions的区别
在 Dio 中,Options
和 BaseOptions
都是用来配置请求的,但作用范围和使用场景不同:
配置对象 | 作用范围 | 常用属性 | 覆盖关系 |
---|---|---|---|
BaseOptions |
Dio 实例全局默认 | baseUrl、headers、timeout、responseType | 被单次请求的 Options 覆盖 |
Options |
单次请求 | method、headers、contentType、validateStatus 等 | 覆盖 BaseOptions 中对应值 |
上传与下载文件
// 下载文件
await dio.download(
"https://example.com/file.png",
"./file.png",
onReceiveProgress: (received, total) {
print("$received/$total");
},
);
// 上传文件
FormData formData = FormData.fromMap({
"file": await MultipartFile.fromFile("path/to/file.png"),
});
await dio.post("/upload", data: formData);
拦截器
final dio = Dio(BaseOptions(baseUrl: "https://jsonplaceholder.typicode.com"));
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
print("请求路径: ${options.path}");
return handler.next(options);
},
onResponse: (response, handler) {
print("响应: ${response.statusCode}");
return handler.next(response);
},
onError: (DioError e, handler) {
print("请求错误: ${e.message}");
return handler.next(e);
},
),
);
什么时候添加拦截器
拦截器一般是在 Dio 实例创建后立刻添加:
原因:拦截器会影响请求/响应的整个生命周期,所以必须在发起请求前添加。
可以添加多个拦截器,顺序按添加顺序执行。
拦截器的作用
Dio 的 拦截器(Interceptor) 可以在请求发出前、响应到达前和请求出错时,统一处理一些逻辑,比如:
- 请求日志打印
- 添加公共请求头
- 请求/响应统一处理错误
- 全局鉴权(Token刷新)
onRequest
触发时机:每次请求发出 前 调用
参数:
options
:包含请求的 URL、方法、headers、queryParameters 等handler
:控制请求继续、停止或返回自定义数据
作用:
- 可以修改请求信息,比如添加统一 Header、修改参数
- 日志打印请求信息
必须调用 handler.next(options)
才会继续请求
onResponse
触发时机:服务器返回响应 成功 时调用(状态码 2xx 或自定义 validateStatus)
参数:
response
:服务器返回的数据、状态码、请求信息handler
:控制响应继续处理
作用:
- 可以统一处理响应数据,比如解包 JSON、日志打印
- 可以根据状态码统一抛错或者做数据转换
必须调用 handler.next(response)
才会把响应继续返回到调用处
onError
触发时机:请求出错时调用(网络错误、超时、响应错误等)
参数:
DioError e
:包含错误类型、请求信息、响应信息handler
:可以继续抛出错误或者返回自定义数据
作用:
- 统一处理错误日志
- 可以做重试、错误提示、Token 刷新等
必须调用 handler.next(e)
或者 handler.resolve(response)
才会继续流程
请求取消
CancelToken cancelToken = CancelToken();
// 发送请求
dio.get("/path", cancelToken: cancelToken);
// 取消请求
cancelToken.cancel("取消请求");
CancelToken
是一个 令牌,请求时绑定这个 token- 调用
cancelToken.cancel()
时:- 请求会立即中止
- Dio 会抛出一个
DioError
,类型为DioErrorType.cancel
- 你可以在
onError
拦截器里捕获并处理取消请求的情况
请求取消的典型场景
- 用户切换页面或关闭页面时,当前页面的请求已经不需要了
- 用户快速连续触发多次搜索,前一次请求的结果不再需要
- 长时间请求超时前手动取消
flowchart LR classDef bigFont font-size:25px; A[发起 Dio 请求]:::bigFont --> B{绑定 CancelToken?}:::bigFont B -- 是 --> C[请求发送到服务器]:::bigFont B -- 否 --> C C --> D{用户或条件触发取消?}:::bigFont D -- 是 --> E[调用 cancelToken.cancel]:::bigFont E --> F[Dio 抛出 DioErrorType.cancel]:::bigFont F --> G[onError 拦截器捕获错误]:::bigFont G --> H[处理取消逻辑,比如忽略响应]:::bigFont D -- 否 --> I[服务器响应正常返回]:::bigFont I --> J[onResponse 拦截器处理数据]:::bigFont J --> K[回调调用处处理响应结果]:::bigFont
Dio与GetXController
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:dio/dio.dart';
// ------------------ 1. 创建 Controller --------------------
class PostController extends GetxController {
// Dio 实例
/*
JSONPlaceholder 的 CORS 或 UA 限制
在 Web 或某些模拟器环境下,如果没有设置请求头,有些公共接口可能会返回 403。
特别是 User-Agent 为空时,有些服务器会拒绝请求。
Dio 默认行为
Dio 默认 validateStatus 会对非 2xx 的响应抛异常。
所以你拿到 403,会直接抛 DioException,不会返回 response.statusCode。
*/
final Dio dio = Dio(
BaseOptions(
baseUrl: "https://jsonplaceholder.typicode.com",
headers: {"User-Agent": "Mozilla/5.0", "Accept": "application/json"},
validateStatus: (status) => status! < 500, // 只对 500+ 抛异常
),
);
// 数据列表
var posts = <dynamic>[].obs;
// 状态
var isLoading = false.obs;
var isEmpty = false.obs;
var error = "".obs;
// 获取数据
void fetchPosts() async {
isLoading.value = true;
isEmpty.value = false;
error.value = "";
try {
final response = await dio.get("/posts");
debugPrint("Status Code: ${response.statusCode}");
if (response.statusCode == 200) {
final data = response.data;
if (data.isEmpty) {
posts.clear();
isEmpty.value = true;
} else {
posts.value = data;
}
} else {
error.value = "请求失败: ${response.statusCode}";
}
} catch (e) {
debugPrint("请求异常: $e");
error.value = e.toString();
} finally {
isLoading.value = false;
}
}
}
// -----------------------2. 创建 Binding --------------------
class PostBinding extends Bindings {
@override
void dependencies() {
// 延迟注入 PostController
Get.lazyPut<PostController>(() => PostController());
}
}
// -----------------------3. 创建页面 ------------------------
class PostPage extends StatelessWidget {
PostPage({super.key});
final PostController controller = Get.find<PostController>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Dio + GetController 示例")),
body: Column(
children: [
ElevatedButton(
onPressed: () {
// 点击按钮触发请求
controller.fetchPosts();
},
child: Text("加载数据"),
),
Expanded(
child: Obx(() {
if (controller.isLoading.value) {
return Center(child: CircularProgressIndicator());
} else if (controller.error.isNotEmpty) {
return Center(child: Text("错误: ${controller.error.value}"));
} else if (controller.posts.isEmpty) {
return Center(child: Text("暂无数据"));
} else {
return ListView.builder(
itemCount: controller.posts.length,
itemBuilder: (context, index) {
final post = controller.posts[index];
return ListTile(
title: Text(post["title"]),
subtitle: Text(post["body"]),
);
},
);
}
}),
),
],
),
);
}
}
// -----------------------4. 路由配置 ------------------------
void main() {
runApp(
GetMaterialApp(
initialRoute: '/posts',
getPages: [
GetPage(name: '/posts', page: () => PostPage(), binding: PostBinding()),
],
),
);
}
