跳转至

06-网络请求 Dio

Dio 是 Flutter / Dart 中最流行的 HTTP 客户端库,功能完整,API 友好。 本章对比 Java 的 HttpClient / RestTemplate 讲解,并介绍 Vexfy 中的 OSS 上传下载用法。


1. Dio 是什么?

Java Dart
HttpURLConnection(底层) HttpClient(Dart 底层)
RestTemplate(Spring 封装) Dio(功能完整封装)
WebClient / Retrofit Dio + 拦截器(类似 Retrofit)
OkHttp Dio 底层默认用 HttpClient,也支持自定义

Dio 核心功能: - GET / POST / PUT / DELETE / PATCH - 文件上传 / 下载(支持进度回调) - 拦截器(请求/响应/错误拦截) - 请求/响应自动 JSON 转换 - 连接池、超时、代理 - 适配器模式(支持自定义 HTTP 引擎)


2. 基本用法

2.1 创建 Dio 实例

import 'package:dio/dio.dart';

final dio = Dio(
  BaseOptions(
    baseUrl: 'https://api.vexfy.com',  // 基础 URL
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 10),
    headers: {
      'Content-Type': 'application/json',
      // 全局 Header
    },
  ),
);

Java 对比:

// RestTemplate(Spring)
RestTemplate restTemplate = new RestTemplate();

// 或者 OkHttp
OkHttpClient client = new OkHttpClient.Builder()
    .connectTimeout(10, TimeUnit.SECONDS)
    .build();

2.2 GET 请求

// 简单 GET
final response = await dio.get('/songs');

// 带查询参数
final response = await dio.get('/songs', queryParameters: {
  'keyword': '周杰伦',
  'page': 1,
  'size': 20,
});

// 响应体
if (response.statusCode == 200) {
  final data = response.data;  // JSON 自动解析为 dynamic
  final List songs = data['songs'];  // 类似 Java 的 Map<String, Object>
}

Java 对比:

// RestTemplate GET
String url = "https://api.vexfy.com/songs?keyword=周杰伦&page=1";
Song[] songs = restTemplate.getForObject(url, Song[].class);

// 或者 URI 构建
Uri uri = Uri.parse("https://api.vexfy.com/songs")
    .replace(queryParameters: {'keyword': '周杰伦'});
Song[] songs = restTemplate.getForObject(uri, Song[].class);

2.3 POST 请求

// POST JSON
final response = await dio.post('/songs', data: {
  'title': '晴天',
  'artist': '周杰伦',
  'duration': 265000,
});

// 上传文件(FormData)
final formData = FormData.fromMap({
  'file': await MultipartFile.fromFile(
    '/path/to/song.mp3',
    filename: 'song.mp3',
  ),
  'song_id': '12345',
});

final response = await dio.post('/upload', data: formData);

Java 对比:

// RestTemplate POST
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);

Map<String, Object> body = new HashMap<>();
body.put("title", "晴天");
body.put("artist", "周杰伦");

HttpEntity<Map<String, Object>> entity = new HttpEntity<>(body, headers);
Song song = restTemplate.postForObject("/songs", entity, Song.class);

// 文件上传
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("file", new FileSystemResource("/path/to/song.mp3"));
restTemplate.postForObject("/upload", parts, UploadResult.class);

3. Vexfy 中的 OSS 上传下载

Vexfy 使用阿里云 OSS(对象存储)存储音频文件和封面图。Dio 负责实际传输。

3.1 OSS 上传流程

本地音乐文件 → Vexfy → 计算文件 Hash → 请求后端获取上传凭证 → 
Dio PUT 上传到 OSS → 获得 OSS URL → 存入数据库

3.2 Dio 下载文件(带进度)

/// 下载 OSS 文件(带进度回调)
/// [url] OSS 文件 URL
/// [savePath] 本地保存路径
Future<void> downloadFile(
  String url,
  String savePath, {
  void Function(int received, int total)? onProgress,
}) async {
  final dio = Dio();

  await dio.download(
    url,
    savePath,
    onReceiveProgress: (received, total) {
      if (total != -1) {
        // 计算百分比
        final percent = received / total;
        onProgress?.call(received, total);
      }
    },
    options: Options(
      // OSS 下载可能需要响应式流
      responseType: ResponseType.stream,
    ),
  );
}

Java 对比:

// Java 下载(使用 Spring RestTemplate 或 Apache HttpClient)
RestTemplate restTemplate = new RestTemplate();

// 下载大文件需要流式处理
Resource resource = restTemplate.getForObject(url, Resource.class);
// 或使用 Apache HttpClient 的 FileDownload

3.3 Dio 上传文件到 OSS

/// 上传文件到 OSS
/// [filePath] 本地文件路径
/// [ossKey] OSS 上的存储路径
/// [onProgress] 进度回调
Future<String> uploadToOss(
  String filePath,
  String ossKey, {
  void Function(int sent, int total)? onProgress,
}) async {
  final dio = Dio();

  // 构造 FormData
  final formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(filePath, filename: ossKey.split('/').last),
    'key': ossKey,
    // 其他 OSS 上传需要的字段(如 policy, signature 等)
  });

  // 上传到自己的后端服务器(由后端转发到 OSS,或提供预签名 URL)
  final response = await dio.post(
    'https://api.vexfy.com/oss/upload',
    data: formData,
    onSendProgress: (sent, total) {
      onProgress?.call(sent, total);
    },
  );

  // 假设返回 OSS URL
  return response.data['url'] as String;
}

4. 拦截器

拦截器是 Dio 最强大的功能之一,类似 Java 的 Filter / Spring AOP。

4.1 请求拦截器

dio.interceptors.add(
  InterceptorsWrapper(
    onRequest: (options, handler) {
      // 每个请求前自动调用
      // 可以在这里统一添加 Token
      final token = getToken();  // 获取 Token
      options.headers['Authorization'] = 'Bearer $token';

      print('🌐 ${options.method} ${options.uri}');
      handler.next(options);  // 继续处理(必须调用)
    },
  ),
);

Java 对比: 类似 Spring 的 OncePerRequestFilter

4.2 响应拦截器

    onResponse: (response, handler) {
      // 每个响应前自动调用
      print('✅ ${response.statusCode} ${response.requestOptions.uri}');
      handler.next(response);  // 继续处理
    },

4.3 错误拦截器

    onError: (error, handler) {
      // 请求失败时调用(网络错误 / HTTP 错误状态码)
      if (error.response?.statusCode == 401) {
        // Token 过期,跳转登录
        goToLogin();
      } else if (error.response?.statusCode == 500) {
        // 服务器错误
        showToast('服务器异常,请稍后重试');
      }
      handler.next(error);  // 或 handler.resolve(response) 消费错误
    },

4.4 Log 拦截器(调试)

import 'package:dio/dio.dart';

dio.interceptors.add(LogInterceptor(
  requestBody: true,
  responseBody: true,
  error: true,
  logPrint: (obj) => print('[Dio] $obj'),
));

5. 错误处理

try {
  final response = await dio.get('/songs');
  if (response.statusCode == 200) {
    final songs = response.data['list'] as List;
    // 处理数据
  }
} on DioException catch (e) {
  // DioException 是 Dio 所有错误的父类
  switch (e.type) {
    case DioExceptionType.connectionTimeout:
      showToast('连接超时,请检查网络');
      break;
    case DioExceptionType.receiveTimeout:
      showToast('服务器响应超时');
      break;
    case DioExceptionType.badResponse:
      // HTTP 错误(如 404, 500)
      print('HTTP ${e.response?.statusCode}');
      break;
    case DioExceptionType.connectionError:
      showToast('网络连接失败');
      break;
    case DioExceptionType.cancel:
      // 请求被取消
      break;
    default:
      showToast('未知错误:${e.message}');
  }
}

Java 对比:

try {
    ResponseEntity<Song[]> response = restTemplate.getForEntity(url, Song[].class);
} catch (HttpClientErrorException e) {
    // 4xx 错误
    if (e.getStatusCode() == HttpStatus.NOT_FOUND) { ... }
} catch (RestClientException e) {
    // 网络错误 / 超时
    e.printStackTrace();
}

6. 小结

功能 Dart Dio Java
HTTP 方法 dio.get/post/put/delete restTemplate.getForObject/postForObject
JSON 自动解析 response.data 手动或 @JsonDeserialize
文件上传 FormData + MultipartFile MultipartFile / Resource
进度回调 onSendProgress / onReceiveProgress 手动流处理
拦截器 InterceptorsWrapper HandlerInterceptor / Filter
超时 connectTimeout / receiveTimeout setConnectTimeout
全局配置 BaseOptions RestTemplateBuilder

下一步

07-数据库sqflite — 了解 Flutter 本地 SQLite 数据库