Skip to content

dio 网络请求

什么是 dio?为什么用它?

Flutter 本身没有内置的网络请求库,我们需要借助第三方包。最基础的选择是 http 包,但当你遇到以下场景时,http 就力不从心了:

  • 每次请求都要手动写 baseUrlheaders,太重复了
  • 想在所有请求里自动加上登录 Token,没有统一的地方插入逻辑
  • 服务器返回了业务错误码(比如 {"code": 4001, "msg": "参数错误"}),需要统一处理
  • 要上传文件、显示上传进度
  • 要下载文件、显示下载进度
  • 请求没响应想自动重试

dio 就是为了解决这些问题而生的。你可以把它理解为一个"增强版的 http 包"——http 能做的它能做,http 做不了的它也能做。

先学哪个?

如果你是刚接触网络请求,建议先快速浏览 HTTP 请求 了解基本概念(GET/POST 是什么、状态码是什么),再来学习 dio。

第一步:安装

在项目根目录下运行:

bash
flutter pub add dio

运行后,pubspec.yaml 中会自动添加依赖:

yaml
dependencies:
  dio: ^5.4.0  # 版本号以实际安装为准

第二步:最简单的请求

先不纠结配置,直接发一个请求感受一下:

dart
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 响应,会自动变成 ListMap
  • response.statusCode 是 HTTP 状态码(200 表示成功)

TIP

dio 会自动根据响应头的 Content-Type 解析 JSON,你不需要手动调用 json.decode()。这比 http 包方便很多。

第三步:配置 BaseOptions(全局配置)

每次都写完整 URL 很麻烦,而且超时时间、请求头这些配置每个请求都要写一遍。dio 提供了 BaseOptions 来统一设置:

dart
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
  },
));

配置之后,请求只需要写相对路径

dart
// 之前:要写完整 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 用于从服务器获取数据,是最常用的请求方式。

dart
// 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 用于向服务器提交新数据,比如注册、登录、创建文章等。

dart
// 提交 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 用于整体替换一条已有数据。

dart
final response = await dio.put('/users/1', data: {
  'name': 'Tom Updated',
  'email': 'tom_new@example.com',
});

PATCH 请求 — 更新数据(部分修改)

PATCH 只修改你提供的字段,不影响其他字段。

dart
// 只修改 name,email 不变
final response = await dio.patch('/users/1', data: {
  'name': 'New Name',
});

DELETE 请求 — 删除数据

dart
final response = await dio.delete('/users/1');

请求方式速查表

方式用途有请求体示例场景
GET获取数据获取用户列表
POST创建新数据注册、登录、发帖
PUT全量更新数据替换整篇文章
PATCH部分更新数据只改用户名
DELETE删除数据通常无删除某篇文章

第五步:拦截器(核心功能)

拦截器是 dio 最强大的功能。简单理解:拦截器就像一个"中间人",在请求发出前或响应到达后,可以统一做一些处理。

一个生活类比

想象你寄快递:

  • 请求拦截器 = 寄件前,统一在包裹上贴上你的地址标签(自动加 Token)
  • 响应拦截器 = 收件后,统一检查包裹是否损坏(统一处理业务错误码)
  • 错误拦截器 = 快递丢了,自动尝试重新寄一次(Token 过期自动刷新)

请求拦截器:自动添加 Token

大部分 API 需要登录后才能访问,要求在请求头带上 Token。如果每个请求都手动加,太麻烦了。用请求拦截器可以自动加:

dart
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 请求头,你不需要每次手动写了:

dart
// 你写的代码
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 的响应格式是这样的:

json
{
  "code": 0,       // 0 表示成功,非 0 表示业务错误
  "message": "ok",
  "data": { ... }  // 实际数据
}

HTTP 状态码是 200,但 code 可能不是 0。我们可以在响应拦截器中统一处理:

dart
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'] 了:

dart
// 之前
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,用户完全无感知:

dart
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 内置了日志拦截器:

dart
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 模式启用。

拦截器添加顺序

如果添加了多个拦截器,执行顺序是:请求拦截按添加顺序执行,响应/错误拦截按相反顺序执行。

dart
// 建议的添加顺序:
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 处理错误

dart
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 来管理加载状态、数据和错误:

dart
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'] ?? ''),
                    );
                  },
                ),
    );
  }
}

第七步:文件上传

单文件上传(比如上传头像)

文件上传需要用 FormDataMultipartFile,这是 HTTP 协议中上传文件的标准格式:

dart
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')是后端接口定义的,必须跟后端保持一致,否则后端收不到数据。

多文件上传

dart
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}');
  }
}

带上传进度

上传大文件时,给用户展示进度条:

dart
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 上传图片

实际项目中最常见的场景——用户选择图片后上传:

dart
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)。

第八步:文件下载

基本下载

dart
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}');
  }
}

带下载进度

dart
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 获取应用目录:

bash
flutter pub add path_provider
dart
import '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}');
  }
}

第九步:请求取消

有时候用户在数据还没加载完时就退出了页面,这时候应该取消请求,避免浪费资源和报错。

dart
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 中使用

dart
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)

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 中初始化

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 类发请求

dart
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.dataMapList
  • ResponseType.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 类。

下一步

  • HTTP 请求 — 了解 http 包的基础用法及 http vs dio 对比
  • JSON 序列化 — 把 dio 返回的 Map 转为 Dart 模型类
  • 本地存储 — Token 等数据的本地持久化方案

基于 Flutter 官方文档整理