网络请求:http 与 dio

Flutter 没有内置功能完备的网络库:官方提供轻量的 http,社区主流是 dio。判断标准很直接——只发几个 GET 请求用 http;需要拦截器、统一鉴权、超时、取消、上传下载进度就用 dio。本章从最小用法讲到请求层封装,重点是把超时、重试、错误转换做对。

依赖安装与权限

flutter pub add http            # 极简
flutter pub add dio             # 功能完整
flutter pub get
# Android 需在 AndroidManifest.xml 声明 android.permission.INTERNET,
# 走明文 HTTP 还要配 usesCleartextTraffic 或网络安全配置

http 的最小用法

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<Map<String, dynamic>> fetchUser(int id) async {
  // 一定要设超时,否则弱网下会一直挂着
  final res = await http.get(Uri.parse('https://jsonplaceholder.typicode.com/users/$id'))
      .timeout(const Duration(seconds: 10));
  if (res.statusCode != 200) throw Exception('请求失败:${res.statusCode}');
  return jsonDecode(utf8.decode(res.bodyBytes)) as Map<String, dynamic>; // utf8 解码防乱码
}

http 的短板:没有拦截器(鉴权头每次手写)、没有取消机制、没有进度回调、异常只能自己判 statusCode。业务一多就该换 dio

dio 的 BaseOptions

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart' show kDebugMode;

final dio = Dio(
  BaseOptions(
    baseUrl: 'https://jsonplaceholder.typicode.com',
    connectTimeout: const Duration(seconds: 10), // 建连超时
    receiveTimeout: const Duration(seconds: 15), // 接收超时
    sendTimeout: const Duration(seconds: 15),    // 发送超时
    headers: {'Accept': 'application/json'},
  ),
);
参数作用建议值
baseUrl统一前缀,后续只写相对路径按环境(dev/staging/prod)注入
connectTimeout / receiveTimeout建连与接收超时上限各 5~20 秒

Interceptor:统一鉴权与错误转换

class AuthInterceptor extends Interceptor {
  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final token = await TokenStore.read(); // 换成自己的取 token 逻辑
    if (token != null) options.headers['Authorization'] = 'Bearer $token';
    handler.next(options); // 必须调用,否则请求不会发出
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) {
    debugPrint('✗ ${err.requestOptions.uri} ${err.message}');
    if (err.response?.statusCode == 401) debugPrint('登录已过期,需要重新登录');
    handler.next(err); // 也可以在这里统一重试
  }
}

// 日志拦截器只用于开发期,上线前务必关掉
dio.interceptors.addAll([AuthInterceptor(), if (kDebugMode) LogInterceptor()]);

DioException:错误分类与提示

DioExceptionType含义用户可读文案
connectionTimeout / receiveTimeout建连或响应超时网络较慢,请稍后重试
badResponse状态码异常statusCode 分 4xx/5xx 提示
cancel被主动取消不提示(用户自己取消的)
connectionError无网络、域名解析失败请检查网络连接

判断时用 switch (error.type)badResponse 下再按 statusCode 区分 401 与 5xx;cancel 必须静默忽略,否则用户主动取消也会弹提示。

上传、下载与取消

// 1. 上传:FormData 组装 multipart,onSendProgress 回报进度
await dio.post('/upload', data: FormData.fromMap({
  'file': await MultipartFile.fromFile(path, filename: 'avatar.png'),
}), onSendProgress: (sent, total) => debugPrint('上传 $sent/$total'));

// 2. 下载:带接收进度
await dio.download('https://example.com/app.apk', savePath,
    onReceiveProgress: (received, total) => debugPrint('下载 $received/$total'));

// 3. 取消:final cancelToken = CancelToken(); 请求传入该 token
// 页面销毁时 cancelToken.cancel('页面已关闭');捕获异常时用 CancelToken.isCancel(e) 静默忽略

请求层封装建议

把网络细节收进 ApiClient,业务层只关心模型,不关心 dio

class ApiException implements Exception {
  ApiException(this.message, {this.code});
  final String message;
  final int? code;
}

class ApiClient {
  ApiClient(this._dio);
  final Dio _dio;

  // 统一解包约定结构 { code, message, data }
  Future<T> request<T>(Future<Response<dynamic>> Function() send, T Function(dynamic) parse) async {
    try {
      final res = await send();
      final body = res.data;
      if (body is Map && body['code'] != null && body['code'] != 0) {
        throw ApiException(body['message'] as String? ?? '业务异常', code: body['code'] as int?);
      }
      return parse(body is Map && body.containsKey('data') ? body['data'] : body);
    } on DioException catch (e) {
      throw ApiException('请求失败', code: e.response?.statusCode);
    } on FormatException {
      throw ApiException('数据解析失败'); // 后端返回了非预期结构
    }
  }
}

配套的三条纪律:所有请求都要有超时并给用户可重试的入口;重试只对幂等的 GET 做,且限制次数与退避间隔(POST 重试可能重复下单);页面销毁时取消在途请求。

flutter pub get

小结:小项目用 http.timeout() 就够,业务一多就切 dio;用 BaseOptions 定 baseUrl 与三类超时,用 Interceptor 统一鉴权与日志,用 DioException.type 把错误翻成人话;上传下载配 FormData 与进度回调,离开页面别忘了 CancelToken.cancel()

笔记加载中…