网络请求: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()。