dio 网络请求
什么是 dio?为什么用它?
Flutter 本身没有内置的网络请求库,我们需要借助第三方包。最基础的选择是 http 包,但当你遇到以下场景时,http 就力不从心了:
- 每次请求都要手动写
baseUrl、headers,太重复了 - 想在所有请求里自动加上登录 Token,没有统一的地方插入逻辑
- 服务器返回了业务错误码(比如
{"code": 4001, "msg": "参数错误"}),需要统一处理 - 要上传文件、显示上传进度
- 要下载文件、显示下载进度
- 请求没响应想自动重试
dio 就是为了解决这些问题而生的。你可以把它理解为一个"增强版的 http 包"——http 能做的它能做,http 做不了的它也能做。
先学哪个?
如果你是刚接触网络请求,建议先快速浏览 HTTP 请求 了解基本概念(GET/POST 是什么、状态码是什么),再来学习 dio。
第一步:安装
在项目根目录下运行:
flutter pub add dio运行后,pubspec.yaml 中会自动添加依赖:
dependencies:
dio: ^5.4.0 # 版本号以实际安装为准第二步:最简单的请求
先不纠结配置,直接发一个请求感受一下:
import 'package:dio/dio.dart';
Future<void> main() async {
// 创建一个 dio 实例
final dio = Dio();
// 发起 GET 请求
final response = await dio.get('https://jsonplaceholder.typicode.com/users');
// 打印响应数据
print(response.data); // 自动解析好的 JSON 数据(List 或 Map)
print(response.statusCode); // 状态码,如 200
}关键点:
dio.get(url)发起 GET 请求,返回一个Response对象response.data是自动解析后的数据,如果是 JSON 响应,会自动变成List或Mapresponse.statusCode是 HTTP 状态码(200 表示成功)
TIP
dio 会自动根据响应头的 Content-Type 解析 JSON,你不需要手动调用 json.decode()。这比 http 包方便很多。
第三步:配置 BaseOptions(全局配置)
每次都写完整 URL 很麻烦,而且超时时间、请求头这些配置每个请求都要写一遍。dio 提供了 BaseOptions 来统一设置:
import 'package:dio/dio.dart';
final dio = Dio(BaseOptions(
baseUrl: 'https://jsonplaceholder.typicode.com', // 基础地址
connectTimeout: const Duration(seconds: 5), // 连接服务器最多等 5 秒
receiveTimeout: const Duration(seconds: 10), // 等待响应最多 10 秒
sendTimeout: const Duration(seconds: 5), // 发送数据最多等 5 秒
headers: {
'Content-Type': 'application/json', // 告诉服务器我们发的是 JSON
'Accept': 'application/json', // 告诉服务器我们希望收到 JSON
},
));配置之后,请求只需要写相对路径:
// 之前:要写完整 URL
final response = await dio.get('https://jsonplaceholder.typicode.com/users');
// 现在:baseUrl + 相对路径,dio 会自动拼接
final response = await dio.get('/users');
// 实际请求的是 https://jsonplaceholder.typicode.com/users超时是什么意思?
- connectTimeout:和服务器建立连接的最长等待时间。如果网络很差,5 秒还连不上就放弃。
- receiveTimeout:连接成功后,等待服务器返回数据的最长时间。如果服务器处理太慢,10 秒还没返回就放弃。
- sendTimeout:发送请求数据的最长时间。上传大文件时可能会用到。
第四步:各种请求方式
GET 请求 — 获取数据
GET 用于从服务器获取数据,是最常用的请求方式。
// 1. 最简单的 GET
final response = await dio.get('/users');
// 2. 带查询参数 —— 相当于访问 /users?page=1&limit=20
final response = await dio.get(
'/users',
queryParameters: {'page': 1, 'limit': 20},
);
// 3. 获取某个用户 —— 路径中带上 ID
final response = await dio.get('/users/1');
// 或者用字符串插值
final userId = 1;
final response = await dio.get('/users/$userId');POST 请求 — 提交数据
POST 用于向服务器提交新数据,比如注册、登录、创建文章等。
// 提交 JSON 数据(最常用)
final response = await dio.post('/users', data: {
'name': 'Tom',
'email': 'tom@example.com',
});
// 提交表单数据(类似网页中的 <form>)
final response = await dio.post(
'/login',
data: {'username': 'admin', 'password': '123456'},
options: Options(contentType: Headers.formUrlEncodedContentType),
);PUT 请求 — 更新数据(全量替换)
PUT 用于整体替换一条已有数据。
final response = await dio.put('/users/1', data: {
'name': 'Tom Updated',
'email': 'tom_new@example.com',
});PATCH 请求 — 更新数据(部分修改)
PATCH 只修改你提供的字段,不影响其他字段。
// 只修改 name,email 不变
final response = await dio.patch('/users/1', data: {
'name': 'New Name',
});DELETE 请求 — 删除数据
final response = await dio.delete('/users/1');请求方式速查表
| 方式 | 用途 | 有请求体 | 示例场景 |
|---|---|---|---|
| GET | 获取数据 | 无 | 获取用户列表 |
| POST | 创建新数据 | 有 | 注册、登录、发帖 |
| PUT | 全量更新数据 | 有 | 替换整篇文章 |
| PATCH | 部分更新数据 | 有 | 只改用户名 |
| DELETE | 删除数据 | 通常无 | 删除某篇文章 |
第五步:拦截器(核心功能)
拦截器是 dio 最强大的功能。简单理解:拦截器就像一个"中间人",在请求发出前或响应到达后,可以统一做一些处理。
一个生活类比
想象你寄快递:
- 请求拦截器 = 寄件前,统一在包裹上贴上你的地址标签(自动加 Token)
- 响应拦截器 = 收件后,统一检查包裹是否损坏(统一处理业务错误码)
- 错误拦截器 = 快递丢了,自动尝试重新寄一次(Token 过期自动刷新)
请求拦截器:自动添加 Token
大部分 API 需要登录后才能访问,要求在请求头带上 Token。如果每个请求都手动加,太麻烦了。用请求拦截器可以自动加:
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// 这段代码会在每个请求发出之前执行
// 从某处获取登录 Token(这里用变量模拟,实际项目通常从本地存储读取)
const token = 'your_login_token_here';
// 如果有 Token,就加到请求头里
if (token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
// 调用 handler.next(options) 表示"继续执行",不要忘了!
handler.next(options);
},
));加了上面的拦截器后,所有请求都会自动带上 Authorization: Bearer xxx 请求头,你不需要每次手动写了:
// 你写的代码
final response = await dio.get('/users');
// 实际发出的请求会自动带上 Authorization 请求头
// 等效于手动写:
// final response = await dio.get(
// '/users',
// options: Options(headers: {'Authorization': 'Bearer your_login_token_here'}),
// );handler 是什么?
拦截器中有三个 handler 方法,必须调用其中一个:
handler.next(options)— 继续执行(最常用)handler.resolve(response)— 直接返回一个假响应,不发送真实请求handler.reject(error)— 直接报错,不发送真实请求
响应拦截器:统一处理业务错误码
很多后端 API 的响应格式是这样的:
{
"code": 0, // 0 表示成功,非 0 表示业务错误
"message": "ok",
"data": { ... } // 实际数据
}HTTP 状态码是 200,但 code 可能不是 0。我们可以在响应拦截器中统一处理:
dio.interceptors.add(InterceptorsWrapper(
onResponse: (response, handler) {
// 这段代码会在每个响应到达后执行
final data = response.data;
// 检查是否是 Map 类型(JSON 解析结果)
if (data is Map) {
if (data['code'] != 0) {
// code 不为 0,说明业务逻辑出错了
// 我们把"业务错误"转为 DioException,统一进入错误处理流程
handler.reject(DioException(
requestOptions: response.requestOptions,
error: data['message'] ?? '请求失败',
));
return; // 重要:记得 return,不要再往下执行
}
// code 为 0,业务正常,把 data 字段直接提取出来
response.data = data['data'];
}
handler.next(response); // 继续传递
},
));这样处理后,你拿到的 response.data 就是干净的 data 字段,不用每次都 response.data['data'] 了:
// 之前
final response = await dio.get('/users');
final users = response.data['data']; // 还要再取一层
// 加了响应拦截器后
final response = await dio.get('/users');
final users = response.data; // 直接就是数据,干净!错误拦截器:Token 过期自动刷新
用户登录后 Token 有效期可能只有 2 小时,过期后服务器会返回 401 状态码。我们可以在错误拦截器中自动刷新 Token,用户完全无感知:
dio.interceptors.add(InterceptorsWrapper(
onError: (error, handler) async {
// 这段代码会在请求出错时执行
if (error.response?.statusCode == 401) {
// 401 表示 Token 过期了
try {
// 尝试用 refreshToken 获取新的 Token
// (这里简化了,实际项目中 refreshToken 通常也存在本地存储中)
final newToken = await _refreshToken();
// 保存新 Token
// Storage.setToken(newToken);
// 更新原请求的 Token
error.requestOptions.headers['Authorization'] = 'Bearer $newToken';
// 用新 Token 重新发送原来的请求
final response = await dio.fetch(error.requestOptions);
// 请求成功了,把结果返回给调用方
handler.resolve(response);
} catch (e) {
// 刷新 Token 也失败了(比如 refreshToken 也过期了),就跳转到登录页
handler.reject(error);
}
} else {
// 不是 401 错误,正常传递
handler.next(error);
}
},
));
// 模拟刷新 Token 的方法
Future<String> _refreshToken() async {
final response = await Dio().post('https://api.example.com/auth/refresh', data: {
'refreshToken': 'your_refresh_token',
});
return response.data['token'];
}日志拦截器:调试利器
开发阶段,你想看到每个请求的详细信息(URL、参数、响应等),dio 内置了日志拦截器:
dio.interceptors.add(LogInterceptor(
request: true, // 打印请求信息
requestHeader: true, // 打印请求头
requestBody: true, // 打印请求体
responseHeader: false, // 不打印响应头(通常太长没用)
responseBody: true, // 打印响应体
error: true, // 打印错误信息
));加了之后,控制台会输出类似这样的日志:
*** Request ***
uri: https://jsonplaceholder.typicode.com/users
method: GET
headers: {Authorization: Bearer xxx}
*** Response ***
statusCode: 200
data: [{id: 1, name: Leanne Graham}, ...]生产环境务必移除!
日志拦截器会在控制台打印所有请求和响应内容,包括 Token 等敏感信息。上线前一定要移除或只在 Debug 模式启用。
拦截器添加顺序
如果添加了多个拦截器,执行顺序是:请求拦截按添加顺序执行,响应/错误拦截按相反顺序执行。
// 建议的添加顺序:
dio.interceptors.add(LogInterceptor()); // 1. 日志(最外层,能看到完整过程)
dio.interceptors.add(authInterceptor); // 2. Token 注入
dio.interceptors.add(businessInterceptor); // 3. 业务码解析(最内层)第六步:错误处理
网络请求随时可能失败——没网、服务器崩了、Token 过期……我们需要妥善处理这些情况。
dio 的错误类型
dio 在请求失败时会抛出 DioException,里面包含了错误类型和详细信息:
| DioExceptionType | 什么意思 | 什么时候发生 |
|---|---|---|
connectionTimeout | 连接超时 | 网络差,连不上服务器 |
sendTimeout | 发送超时 | 上传大文件时发送太慢 |
receiveTimeout | 响应超时 | 服务器处理太慢 |
badResponse | 服务器返回了错误状态码 | 404、500 等 |
connectionError | 连接错误 | 没开网络、DNS 解析失败 |
cancel | 请求被取消 | 你主动取消了请求 |
unknown | 未知错误 | 其他异常 |
用 try-catch 处理错误
Future<void> fetchUsers() async {
try {
final response = await dio.get('/users');
// 请求成功,处理数据
print(response.data);
} on DioException catch (e) {
// dio 相关的错误
if (e.type == DioExceptionType.connectionTimeout ||
e.type == DioExceptionType.receiveTimeout) {
print('请求超时了,请检查网络后重试');
} else if (e.type == DioExceptionType.connectionError) {
print('网络连接失败,请检查网络设置');
} else if (e.type == DioExceptionType.badResponse) {
// 服务器返回了错误状态码,可以进一步判断
final statusCode = e.response?.statusCode;
if (statusCode == 404) {
print('请求的资源不存在');
} else if (statusCode == 500) {
print('服务器内部错误');
} else {
print('请求失败,状态码: $statusCode');
}
}
} catch (e) {
// 其他类型的错误
print('未知错误: $e');
}
}在 UI 中展示错误
实际项目中,我们通常用 StatefulWidget 来管理加载状态、数据和错误:
class UserListPage extends StatefulWidget {
const UserListPage({super.key});
@override
State<UserListPage> createState() => _UserListPageState();
}
class _UserListPageState extends State<UserListPage> {
List<dynamic> _users = []; // 用户数据
bool _isLoading = true; // 是否正在加载
String? _errorMessage; // 错误信息
@override
void initState() {
super.initState();
_loadUsers(); // 页面初始化时加载数据
}
Future<void> _loadUsers() async {
setState(() {
_isLoading = true; // 开始加载
_errorMessage = null; // 清空旧错误
});
try {
final response = await dio.get('/users');
setState(() {
_users = response.data; // 保存数据
_isLoading = false; // 加载完成
});
} on DioException catch (e) {
setState(() {
// 根据错误类型生成用户友好的提示
_errorMessage = _getErrorMessage(e);
_isLoading = false;
});
}
}
// 把错误类型转换为用户能看懂的中文提示
String _getErrorMessage(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return '请求超时,请稍后重试';
case DioExceptionType.connectionError:
return '网络连接失败,请检查网络';
case DioExceptionType.badResponse:
final code = e.response?.statusCode;
return '服务器错误 ($code)';
case DioExceptionType.cancel:
return '请求已取消';
default:
return '未知错误,请稍后重试';
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('用户列表')),
body: _isLoading
? const Center(child: CircularProgressIndicator()) // 加载中
: _errorMessage != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_errorMessage!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadUsers, // 点击重试
child: const Text('重试'),
),
],
),
)
: ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
title: Text(user['name'] ?? ''),
subtitle: Text(user['email'] ?? ''),
);
},
),
);
}
}第七步:文件上传
单文件上传(比如上传头像)
文件上传需要用 FormData 和 MultipartFile,这是 HTTP 协议中上传文件的标准格式:
import 'package:dio/dio.dart';
Future<void> uploadAvatar(String filePath) async {
// 第一步:构建 FormData
final formData = FormData.fromMap({
// 'file' 是后端要求的字段名,跟后端确认!
'file': await MultipartFile.fromFile(
filePath, // 本地文件路径
filename: 'avatar.jpg', // 上传后的文件名
),
// 可以同时传其他普通字段
'userId': '123',
});
// 第二步:发送请求
try {
final response = await dio.post('/upload/avatar', data: formData);
print('上传成功: ${response.data}');
} on DioException catch (e) {
print('上传失败: ${e.message}');
}
}字段名要跟后端确认
FormData.fromMap 中的 key(如 'file'、'userId')是后端接口定义的,必须跟后端保持一致,否则后端收不到数据。
多文件上传
Future<void> uploadMultipleFiles(List<String> filePaths) async {
// 把多个文件放到一个列表中
final files = <MultipartFile>[];
for (final path in filePaths) {
files.add(await MultipartFile.fromFile(path));
}
final formData = FormData.fromMap({
'files': files, // 'files' 是后端要求的字段名
'userId': '123',
});
try {
final response = await dio.post('/upload/batch', data: formData);
print('批量上传成功');
} on DioException catch (e) {
print('批量上传失败: ${e.message}');
}
}带上传进度
上传大文件时,给用户展示进度条:
Future<void> uploadWithProgress(String filePath) async {
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(filePath),
});
try {
await dio.post(
'/upload',
data: formData,
onSendProgress: (sent, total) {
// sent: 已发送字节数
// total: 总字节数
if (total > 0) {
final percent = (sent / total * 100).toStringAsFixed(0);
print('上传进度: $percent%');
// 在 UI 中更新进度条:
// setState(() { _progress = sent / total; });
}
},
);
print('上传完成!');
} on DioException catch (e) {
print('上传失败: ${e.message}');
}
}配合 image_picker 上传图片
实际项目中最常见的场景——用户选择图片后上传:
import 'package:image_picker/image_picker.dart';
import 'package:dio/dio.dart';
Future<void> pickAndUpload() async {
// 1. 让用户选择图片
final picker = ImagePicker();
final image = await picker.pickImage(source: ImageSource.gallery);
if (image == null) return; // 用户取消了选择
// 2. 上传图片
final formData = FormData.fromMap({
'file': await MultipartFile.fromFile(
image.path,
filename: image.name,
),
});
try {
final response = await dio.post('/upload/avatar', data: formData);
print('上传成功!');
} on DioException catch (e) {
print('上传失败: ${e.message}');
}
}INFO
使用 image_picker 需要单独安装:flutter pub add image_picker,并配置对应平台的权限(Android 的 AndroidManifest.xml、iOS 的 Info.plist)。
第八步:文件下载
基本下载
Future<void> downloadFile() async {
try {
await dio.download(
'https://example.com/files/report.pdf', // 下载地址
'/path/to/save/report.pdf', // 本地保存路径
);
print('下载完成!');
} on DioException catch (e) {
print('下载失败: ${e.message}');
}
}带下载进度
await dio.download(
'https://example.com/files/report.pdf',
'/path/to/save/report.pdf',
onReceiveProgress: (received, total) {
// received: 已下载字节数
// total: 总字节数(如果服务器没返回 Content-Length,total 为 -1)
if (total > 0) {
final percent = (received / total * 100).toStringAsFixed(0);
print('下载进度: $percent%');
}
},
);下载到应用目录(推荐)
直接写死保存路径在不同手机上可能没权限。推荐使用 path_provider 获取应用目录:
flutter pub add path_providerimport 'package:path_provider/path_provider.dart';
import 'package:dio/dio.dart';
Future<void> downloadToAppDir(String url, String fileName) async {
// 获取应用的文档目录(Android 和 iOS 都有权限写入)
final dir = await getApplicationDocumentsDirectory();
final savePath = '${dir.path}/$fileName';
try {
await dio.download(
url,
savePath,
onReceiveProgress: (received, total) {
if (total > 0) {
print('下载: ${(received / total * 100).toStringAsFixed(0)}%');
}
},
);
print('文件已保存到: $savePath');
} on DioException catch (e) {
print('下载失败: ${e.message}');
}
}第九步:请求取消
有时候用户在数据还没加载完时就退出了页面,这时候应该取消请求,避免浪费资源和报错。
import 'package:dio/dio.dart';
// 创建一个 CancelToken
final cancelToken = CancelToken();
// 发起请求时传入 cancelToken
dio.get('/users', cancelToken: cancelToken).then((response) {
print('请求成功: ${response.data}');
}).catchError((error) {
if (error is DioException && CancelToken.isCancel(error)) {
print('请求被取消了: ${error.message}');
} else {
print('请求出错: $error');
}
});
// 在需要的时候取消请求(比如页面关闭时)
cancelToken.cancel('用户离开了页面');在 StatefulWidget 中使用
class _MyPageState extends State<MyPage> {
CancelToken? _cancelToken; // 保存 cancelToken
@override
void initState() {
super.initState();
_cancelToken = CancelToken();
_loadData();
}
Future<void> _loadData() async {
try {
final response = await dio.get('/users', cancelToken: _cancelToken);
if (mounted) {
setState(() {
// 更新 UI
});
}
} on DioException catch (e) {
if (CancelToken.isCancel(e)) {
// 请求被取消,不做处理
return;
}
// 其他错误处理...
}
}
@override
void dispose() {
// 页面销毁时取消请求!
_cancelToken?.cancel('页面已关闭');
super.dispose();
}
}为什么页面关闭时要取消请求?
如果不取消,请求完成后代码还会尝试调用 setState(),但页面已经不在了,就会报 setState() called after dispose() 的错误。
实战:完整项目封装
前面的代码都是零散的示例,下面展示如何在一个真实项目中组织 dio 相关代码。
文件结构
lib/
├── main.dart
├── http/
│ └── http.dart # dio 封装类
├── models/
│ └── user.dart # 数据模型
└── pages/
└── user_list_page.dart # 页面1. 封装 Http 类 (lib/http/http.dart)
import 'package:dio/dio.dart';
class Http {
// 单例模式:整个 App 只有一个 Dio 实例
Http._internal();
static final Http _instance = Http._internal();
factory Http() => _instance;
late final Dio _dio;
/// 初始化,在 main() 中调用一次
void init({required String baseUrl}) {
_dio = Dio(BaseOptions(
baseUrl: baseUrl,
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 5),
headers: {'Content-Type': 'application/json'},
));
// 添加拦截器
_dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// 自动添加 Token
// final token = await Storage.getToken();
// if (token != null) {
// options.headers['Authorization'] = 'Bearer $token';
// }
handler.next(options);
},
onError: (error, handler) {
// 统一错误处理
_handleError(error);
handler.next(error);
},
));
// 开发时添加日志
_dio.interceptors.add(LogInterceptor(
requestBody: true,
responseBody: true,
));
}
/// GET 请求
Future<T> get<T>(String path, {Map<String, dynamic>? params}) async {
final response = await _dio.get<T>(path, queryParameters: params);
return response.data as T;
}
/// POST 请求
Future<T> post<T>(String path, {dynamic data}) async {
final response = await _dio.post<T>(path, data: data);
return response.data as T;
}
/// PUT 请求
Future<T> put<T>(String path, {dynamic data}) async {
final response = await _dio.put<T>(path, data: data);
return response.data as T;
}
/// DELETE 请求
Future<T> delete<T>(String path, {Map<String, dynamic>? params}) async {
final response = await _dio.delete<T>(path, queryParameters: params);
return response.data as T;
}
/// 统一错误提示
void _handleError(DioException e) {
final msg = switch (e.type) {
DioExceptionType.connectionTimeout ||
DioExceptionType.sendTimeout ||
DioExceptionType.receiveTimeout => '请求超时,请稍后重试',
DioExceptionType.connectionError => '网络连接失败,请检查网络',
DioExceptionType.badResponse => '服务器错误 (${e.response?.statusCode})',
DioExceptionType.cancel => '',
_ => '请求失败,请稍后重试',
};
if (msg.isNotEmpty) {
// 实际项目中用 toast/snackbar 展示,这里先 print
print('[网络错误] $msg');
}
}
}2. 在 main.dart 中初始化
import 'package:flutter/material.dart';
import 'http/http.dart';
void main() {
// 在 App 启动前初始化 Http,设置 baseUrl
Http().init(baseUrl: 'https://jsonplaceholder.typicode.com');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Dio 示例',
home: const UserListPage(),
);
}
}3. 使用 Http 类发请求
import 'package:flutter/material.dart';
import 'http/http.dart';
class UserListPage extends StatefulWidget {
const UserListPage({super.key});
@override
State<UserListPage> createState() => _UserListPageState();
}
class _UserListPageState extends State<UserListPage> {
List<Map<String, dynamic>> _users = [];
bool _isLoading = true;
String? _error;
@override
void initState() {
super.initState();
_loadUsers();
}
Future<void> _loadUsers() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
// 使用封装好的 Http 类发请求
final data = await Http().get<List<dynamic>>('/users');
setState(() {
_users = data.cast<Map<String, dynamic>>();
_isLoading = false;
});
} on DioException catch (e) {
setState(() {
_error = e.message ?? '加载失败';
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('用户列表')),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_error!, style: const TextStyle(color: Colors.red)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _loadUsers,
child: const Text('重试'),
),
],
),
)
: RefreshIndicator(
onRefresh: _loadUsers, // 下拉刷新
child: ListView.builder(
itemCount: _users.length,
itemBuilder: (context, index) {
final user = _users[index];
return ListTile(
leading: CircleAvatar(
child: Text('${user['id']}'),
),
title: Text(user['name'] ?? ''),
subtitle: Text(user['email'] ?? ''),
);
},
),
),
);
}
}常见问题
1. 请求报 DioExceptionType.connectionError
最常见的原因:
- 手机/模拟器没有联网
- 没有配置网络权限(Android 需要在
AndroidManifest.xml中添加<uses-permission android:name="android.permission.INTERNET"/>) - Android 9 以上默认不允许 HTTP 明文请求,需要在
AndroidManifest.xml中配置android:usesCleartextTraffic="true"或使用 HTTPS
2. 请求返回的数据格式不对
检查 BaseOptions 中的 responseType:
ResponseType.json(默认):自动解析 JSON,response.data是Map或ListResponseType.plain:不解析,response.data是字符串ResponseType.bytes:返回原始字节数据,下载文件时用ResponseType.stream:返回数据流,处理大文件时用
3. 为什么有些请求需要 contentType: Headers.formUrlEncodedContentType?
取决于后端接口的设计:
- 大部分现代 API 接收 JSON 格式 → 用默认的就行
- 部分旧接口或 OAuth 登录接口要求表单格式 → 需要设置
contentType: Headers.formUrlEncodedContentType
4. response.data 的类型是什么?
取决于响应内容:
- JSON 数组 →
List<dynamic> - JSON 对象 →
Map<String, dynamic> - 纯文本 →
String
建议配合 JSON 序列化 将 Map/List 转换为类型安全的 Model 类。
