#Flutter HTTP 请求完整详解
1. 前言
Flutter 网络请求分为两种方案:
- dart:io HttpClient:Dart 原生内置,无需第三方包,API 繁琐,适合简单临时请求;
- dio:第三方成熟网络库,工业级项目标准,支持拦截器、超时、取消、文件上传下载、并发、表单,开发首选。
库对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| HttpClient | 无依赖,无需引入包 | API底层,代码量大,无拦截器,不支持便捷表单/上传 | 简单测试、极简工具 |
| dio | API简洁、拦截器、统一配置、上传下载、取消请求 | 需要引入依赖 | 企业正式项目 |
2. Dart 原生 HttpClient
无需 pub 依赖,仅导入 dart:io、dart:convert
2.1 GET 请求示例
dart
import 'dart:io';
import 'dart:convert';
void nativeGet() async {
// 创建客户端
HttpClient client = HttpClient();
try {
// 打开url
HttpClientRequest req = await client.getUrl(Uri.parse("https://httpbin.org/get?name=test"));
// 发送请求
HttpClientResponse res = await req.close();
// 读取响应流
String responseStr = await res.transform(utf8.decoder).join();
Map data = jsonDecode(responseStr);
print("返回数据:$data");
} catch (e) {
print("请求异常:$e");
} finally {
client.close();
}
}
2.2 POST JSON 请求
dart
void nativePost() async {
HttpClient client = HttpClient();
Map params = {"username":"admin","pwd":"123456"};
try {
HttpClientRequest req = await client.postUrl(Uri.parse("https://httpbin.org/post"));
// 设置请求头json
req.headers.set("Content-Type", "application/json");
// 写入json字符串
req.write(jsonEncode(params));
HttpClientResponse res = await req.close();
String str = await res.transform(utf8.decoder).join();
print(jsonDecode(str));
} finally {
client.close();
}
}
原生缺点:无统一拦截、无全局 baseUrl、文件上传代码复杂、无便捷取消请求,项目统一封装成本极高。
3. dio 基础配置
3.1 pubspec.yaml 引入依赖
yaml
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0
执行 flutter pub get 安装依赖
3.2 基础全局 Dio 实例创建
dart
import 'package:dio/dio.dart' as dio;
// 全局单例dio
d.Dio createDio() {
final dio.Dio dio = dio.Dio();
// 全局基础配置
dio.options = dio.BaseOptions(
baseUrl: "https://httpbin.org", // 统一域名前缀
connectTimeout: const Duration(seconds: 10), // 连接超时
receiveTimeout: const Duration(seconds: 10), // 接收超时
sendTimeout: const Duration(seconds: 10),
headers: {
"Content-Type": "application/json",
"token": "全局登录token",
},
);
return dio;
}
final dio.Dio http = createDio();
4. dio 常用请求方式
4.1 GET 请求
dart
// 方式1:url拼接
await http.get("/get?name=张三&age=20");
// 方式2:params 自动拼接(推荐,自动处理编码)
var res = await http.get("/get", queryParameters: {
"name": "张三",
"age": 20
});
print(res.data); // res.data 自动转Map/List
print(res.statusCode); // 状态码 200/404/500
4.2 POST JSON 提交
dart
var res = await http.post("/post", data: {
"username": "test",
"password": "123456"
});
Map result = res.data;
4.3 POST 表单提交 x-www-form-urlencoded
dart
var formData = dio.FormData.fromMap({
"account": "admin",
"pwd": "666666"
});
await http.post("/login", data: formData);
4.4 携带自定义请求头
dart
await http.get("/user/info", options: dio.Options(
headers: {
"Authorization": "Bearer xxx-token",
"device": "android"
}
));
5. dio 拦截器
拦截器分为三种:请求拦截、响应拦截、错误拦截
作用:统一添加 token、统一打印日志、统一处理登录过期、统一格式化返回数据
dart
http.interceptors.add(d.InterceptorsWrapper(
onRequest: (d.RequestOptions options, d.RequestInterceptorHandler handler) {
// 请求前统一处理
print("请求地址:${options.path}");
// 自动追加token
options.headers["token"] = "local_token_123";
return handler.next(options);
},
onResponse: (d.Response response, d.ResponseInterceptorHandler handler) {
// 统一解析后端返回格式
print("响应数据:${response.data}");
return handler.next(response);
},
onError: (d.DioException err, d.ErrorInterceptorHandler handler) {
// 统一捕获所有网络异常
print("网络错误:${err.message}");
// 登录过期跳转登录页
if (err.response?.statusCode == 401) {
// 跳转登录逻辑
}
return handler.next(err);
}
));
6 文件上传 & 文件下载
6.1 单文件上传
dart
// FormData 携带文件 + 普通参数
d.FormData formData = d.FormData.fromMap({
"desc": "图片描述",
"file": await d.MultipartFile.fromFile(
"/storage/emulated/0/test.png", // 文件本地路径
filename: "upload.png"
)
});
await http.post("/upload", data: formData);
6.2 多文件上传
dart
List<d.MultipartFile> files = [];
files.add(await d.MultipartFile.fromFile("文件1路径"));
files.add(await d.MultipartFile.fromFile("文件2路径"));
d.FormData data = d.FormData({"files": files});
await http.post("/multi/upload", data: data);
6.3 文件下载
dart
String savePath = "/storage/emulated/0/download/app.apk";
await http.download(
"https://xxx/file.apk",
savePath,
onReceiveProgress: (int received, int total) {
// 下载进度
double progress = received / total;
print("下载进度:${progress * 100}%");
}
);
7 高级功能:超时、取消请求、并发请求
7.1 单次请求单独覆盖超时
dart
await http.get("/api", options: d.Options(
connectTimeout: Duration(seconds: 3)
));
7.2 取消请求(页面销毁终止网络,防内存泄漏)
dart
// 创建取消令牌
d.CancelToken cancelToken = d.CancelToken();
// 发起请求
http.get("/long/api", cancelToken: cancelToken).catchError((e){
if (e is d.DioException && e.type == d.DioExceptionType.cancel) {
print("请求已手动取消");
}
});
// 页面关闭时取消请求
cancelToken.cancel("页面退出,取消请求");
7.3 并发请求
dart
// 同时发起多个请求,全部完成后再处理
Future.wait([
http.get("/banner"),
http.get("/goods/list"),
]).then((List<d.Response> resList) {
var bannerData = resList[0].data;
var goodsData = resList[1].data;
});
8 JSON 数据解析
8.1 简易手动解析
后端标准格式示例
json
{
"code": 200,
"msg": "success",
"data": {
"id": 1,
"username": "张三"
}
}
dart
var res = await http.get("/user/1");
Map json = res.data;
int code = json["code"];
String msg = json["msg"];
Map userInfo = json["data"];
String name = userInfo["username"];
8.2 自动序列化模型类
使用工具 json_serializable 自动生成 fromJson/toJson
- 依赖
yaml
dependencies:
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.4
json_serializable: ^6.7.0
- 用户模型示例 UserModel
dart
import 'package:json_annotation/json_annotation.dart';
part 'user_model.g.dart';
@JsonSerializable()
class UserModel {
final int id;
final String username;
UserModel({required this.id, required this.username});
factory UserModel.fromJson(Map<String> json) => _$UserModelFromJson(json);
}
- 生成命令
bash
flutter pub run build_runner build
- 使用
dart
Map data = res.data["data"];
UserModel user = UserModel.fromJson(data);
print(user.username);
9 高频踩坑与解决方案
9.1 Android 真机无法访问 http 明文接口
Android9+ 默认禁止 HTTP 明文,只允许 HTTPS
解决方案:android/app/src/main/AndroidManifest.xml 添加配置
xml
<application android:usesCleartextTraffic="true" ...>
9.2 iOS http 接口无法请求
Info.plist 添加
xml
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
9.3 跨域、401 token 失效
拦截器统一捕获 401,清除本地存储 token,跳转登录页面
9.4 内存泄漏:页面退出请求未取消
页面 dispose 中调用 cancelToken.cancel()
9.5 上传图片过大超时
单独设置上传接口 sendTimeout 延长
9.6 中文乱码
dio 默认自动处理 utf8,极少出现;如出现手动设置 headers Accept-Charset: utf-8
10 项目级完整封装
统一返回格式、统一异常捕获、拦截器、全局 token、封装 get/post/upload
dart
import 'package:dio/dio.dart';
// 全局单例工具类
class HttpUtil {
static final HttpUtil _instance = HttpUtil._internal();
factory HttpUtil() => _instance;
late Dio dio;
HttpUtil._internal() {
BaseOptions options = BaseOptions(
baseUrl: "https://httpbin.org",
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
headers: {"Content-Type": "application/json"},
);
dio = Dio(options);
// 添加拦截器
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
// 全局token
options.headers["token"] = "local_token";
print("请求url:${options.path} 参数:${options.data ?? options.queryParameters}");
return handler.next(options);
},
onResponse: (resp, handler) {
print("响应:${resp.data}");
return handler.next(resp);
},
onError: (err, handler) {
// 统一错误提示
String errMsg = "网络异常";
if (err.type == DioExceptionType.connectionTimeout) {
errMsg = "连接超时";
} else if (err.response?.statusCode == 401) {
errMsg = "登录失效,请重新登录";
// 跳转登录逻辑
}
print("请求错误:$errMsg");
return handler.next(err);
},
));
}
// GET封装
Future<dynamic> get(String path, {Map? params, CancelToken? token}) async {
Response res = await dio.get(path, queryParameters: params, cancelToken: token);
return res.data;
}
// POST JSON封装
Future<dynamic> post(String path, {Map? data, CancelToken? token}) async {
Response res = await dio.post(path, data: data, cancelToken: token);
return res.data;
}
// 文件上传
Future<dynamic> upload(String path, MultipartFile file) async {
FormData formData = FormData.fromMap({"file": file});
return await dio.post(path, data: formData);
}
// 文件下载
Future download(String url, String savePath, {Function? onProgress}) async {
return await dio.download(url, savePath, onReceiveProgress: (rec, total) {
if(onProgress != null) on(rec / total);
});
}
}
// 使用
final http = HttpUtil();
void testApi() async {
var data = await http.get("/get", params: {"name":"测试"});
print(data);
}
11 拓展
11.1 前置全部依赖
pubspec.yaml
yaml
```yaml
dependencies:
flutter:
sdk: flutter
dio: ^5.7.0
hive: ^2.2.3
hive_flutter: ^1.1.0
flutter_riverpod: ^2.5.1
shared_preferences: ^2.2.2
dev_dependencies:
hive_generator: ^1.1.5
build_runner: ^2.4.6
11.2 全局 Loading 弹窗封装
全局单例 Loading,任意页面调用,请求自动开启、结束自动关闭
loading_util.dart
dart
import 'package:flutter/material';
class LoadingUtil {
static final LoadingUtil _instance = LoadingUtil._internal();
factory LoadingUtil() => _instance;
LoadingUtil._internal();
bool _isShow = false;
// 显示加载弹窗
void show(BuildContext context, {String msg = "加载中..."}) {
if (_isShow) return;
_isShow = true;
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) {
return PopScope(
canPop: false,
child: Center(
child: Container(
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(10),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(color: Colors.white),
SizedBox(height: 10),
Text(msg, style: TextStyle(color: Colors.white)),
],
),
),
),
);
},
);
}
// 关闭弹窗
void dismiss(BuildContext context) {
if (!_isShow) return;
_isShow = false;
Navigator.pop(context);
}
}
集成到 Dio 拦截器:请求前自动 show,成功 / 失败统一 dismiss
11.3 、请求重试拦截器
实现:网络错误 / 5xx 服务端错误自动重试,可配置重试次数、间隔
interceptor/retry_interceptor.dart
dart
import 'package:dio/dio.dart';
class RetryInterceptor extends Interceptor {
// 最大重试次数
final int maxRetry;
// 重试间隔 毫秒
final int delayMs;
RetryInterceptor({this.maxRetry = 2, this.delayMs = 1000});
@override
Future onError(DioException err, ErrorInterceptorHandler handler) async {
// 不需要重试的场景:取消请求、4xx业务错误、上传下载
bool needRetry = _shouldRetry(err);
if (!needRetry) return handler.next(err);
// 获取当前重试次数
int retryCount = err.requestOptions.extra["retryCount"] ?? 0;
if (retryCount >= maxRetry) return handler.next(err);
// 延迟等待
await Future.delayed(Duration(milliseconds: delayMs));
// 重试计数+1
err.requestOptions.extra["retryCount"] = retryCount + 1;
// 重新发起请求
final res = await Dio().fetch(err.requestOptions);
return handler.resolve(res);
}
bool _shouldRetry(DioException err) {
// 取消请求不重试
if (err.type == DioExceptionType.cancel) return false;
// 无网络、连接超时、服务端500以上错误才重试
if (err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.sendTimeout ||
(err.response?.statusCode != null && err.response!.statusCode! >= 500)) {
return true;
}
return false;
}
}
11.4、Hive 持久化缓存拦截器
思路:
- 每个接口 url + 参数 作为唯一缓存 key
- 可自定义缓存有效期
- 请求优先读缓存,无缓存再走网络;网络成功更新缓存
interceptor/cache_interceptor.dart
dart
import 'package:dio/dio.dart';
import 'package:hive/hive';
class CacheInterceptor extends Interceptor {
// 缓存盒子名称
static const String cacheBoxName = "api_cache";
// 默认缓存过期时间 毫秒 30分钟
final int defaultExpire = 30 * 60 * 1000;
// 生成唯一缓存key
String _buildCacheKey(RequestOptions options) {
String path = options.path;
Map query = options.queryParameters;
Map data = options.data ?? {};
return "$path|$query|$data";
}
@override
Future onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
// 跳过缓存标记:extra["noCache"]=true
bool noCache = options.extra["noCache"] ?? false;
if (noCache) return handler.next(options);
var box = await Hive.openBox(cacheBoxName);
String key = _buildCacheKey(options);
var cacheData = box.get(key);
if (cacheData != null) {
int cacheTime = cacheData["cacheTime"];
int expire = options.extra["cacheExpire"] ?? defaultExpire;
int now = DateTime.now().millisecondsSinceEpoch;
// 未过期,直接返回缓存数据,不发起网络请求
if (now - cacheTime < expire) {
Response cacheResp = Response(
data: cacheData["data"],
statusCode: 200,
requestOptions: options,
);
return handler.resolve(cacheResp);
}
}
// 缓存过期/无缓存,正常走网络
return handler.next(options);
}
@override
Future onResponse(Response response, ResponseInterceptorHandler handler) async {
RequestOptions opts = response.requestOptions;
bool noCache = opts.extra["noCache"] ?? false;
if (!noCache) {
String key = _buildCacheKey(opts);
var box = await Hive.openBox(cacheBoxName);
// 存储数据+当前时间戳
await box.put(key, {
"data": response.data,
"cacheTime": DateTime.now().millisecondsSinceEpoch
});
}
return handler.next(response);
}
}
11.5 改造 HttpUtil
http/http_util.dart
dart
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive';
import 'cache_interceptor.dart';
import 'retry_interceptor.dart';
import '../util/loading_util.dart';
class HttpUtil {
static final HttpUtil _ins = HttpUtil._internal();
factory HttpUtil() => _ins;
late Dio dio;
HttpUtil._internal() {
BaseOptions options = BaseOptions(
baseUrl: "https://api.example.com",
connectTimeout: Duration(seconds: 10),
receiveTimeout: Duration(seconds: 10),
headers: {"Content-Type": "application/json"},
);
dio = Dio(options);
// 拦截器顺序:缓存 -> 重试 -> 全局请求/响应/错误拦截
dio.interceptors.add(CacheInterceptor());
dio.interceptors.add(RetryInterceptor(maxRetry: 2, delayMs: 800));
dio.interceptors.add(InterceptorsWrapper(
onRequest: (opts, handler) {
// 自动弹出loading
LoadingUtil().show(navigatorKey.currentContext!, msg: "加载中...");
// 全局token
opts.headers["token"] = Hive.box("user").get("token") ?? "";
return handler.next(opts);
},
onResponse: (resp, handler) {
LoadingUtil().dismiss(navigatorKey.currentContext!);
return handler.next(resp);
},
onError: (err, handler) {
LoadingUtil().dismiss(navigatorKey.currentContext!);
String msg = "网络请求失败";
if (err.type == DioExceptionType.connectionTimeout) msg = "连接超时";
if (err.response?.statusCode == 401) {
// 清除token,跳转登录
Hive.box("user").delete("token");
}
print("接口异常:$msg");
return handler.next(err);
},
));
}
// GET 支持缓存配置 noCache/cacheExpire
Future get(String path,
{Map? params, bool noCache = false, int? cacheExpire, CancelToken? token}) async {
Options opts = Options(extra: {
"noCache": noCache,
if (cacheExpire != null) "cacheExpire": cacheExpire
});
Response res = await dio.get(path, queryParameters: params, options: opts, cancelToken: token);
return res.data;
}
// POST 默认不读缓存
Future post(String path, {Map? data, CancelToken? token}) async {
Options opts = Options(extra: {"noCache": true});
Response res = await dio.post(path, data: data, options: opts, cancelToken: token);
return res.data;
}
// 文件上传
Future upload(String path, MultipartFile file) async {
FormData form = FormData.fromMap({"file": file});
return await dio.post(path, data: form, options: Options(extra: {"noCache": true}));
}
}
// 全局navigatorKey 无需传context展示loading
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
main.dart 配置全局 navigatorKey
dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Hive.initFlutter();
await Hive.openBox("user");
await Hive.openBox("api_cache");
runApp(
MaterialApp(
navigatorKey: navigatorKey,
home: MyHome(),
),
);
}
11.6 Riverpod 封装全局请求状态(AsyncValue)
核心:利用 Riverpod AsyncNotifier 天然携带 loading /data/error,无需手动维护状态
provider/api_provider.dart
dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../http/http_util.dart';
// 通用分页/数据接口示例
final userListProvider = AsyncNotifierProvider<UserListNotifier, List<dynamic>>(UserListNotifier.new);
class UserListNotifier extends AsyncNotifier<List<dynamic>> {
final HttpUtil http = HttpUtil();
// 刷新接口(下拉刷新调用)
Future<void> refresh() async {
state = const AsyncLoading();
try {
var res = await http.get("/user/list", noCache: true);
List list = res["data"];
state = AsyncData(list);
} catch (e) {
state = AsyncError(e, StackTrace.current);
}
}
// 初次加载
@override
Future<List<dynamic>> build() async {
var res = await http.get("/user/list");
return res["data"];
}
}
页面使用 Riverpod 状态
dart
class UserPage extends ConsumerWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
AsyncValue<List<dynamic>> userState = ref.watch(userListProvider);
return Scaffold(
appBar: AppBar(title: Text("用户列表")),
body: userState.when(
loading: () => SizedBox(), // loading弹窗由http拦截器自动管理
error: (err, stack) => Center(child: Text("加载失败:$err")),
data: (list) => ListView.builder(
itemCount: list.length,
itemBuilder: (ctx, idx) => ListTile(title: Text(list[idx]["name"])),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(userListProvider.notifier).refresh(),
child: Icon(Icons.refresh),
),
);
}
}
单次临时请求(无持久状态)
dart
ElevatedButton(
onPressed: () async {
final res = await ref.read(HttpUtil()).get("/profile");
},
child: Text("获取个人信息"),
)
11.7 Provider + ChangeNotifier 请求状态封装
不使用 Riverpod 项目可选用
store/api_store.dart
dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../http/http_util.dart';
class ApiStore extends ChangeNotifier {
bool loading = false;
dynamic data;
String? errorMsg;
Future fetchUserList() async {
loading = true;
errorMsg = null;
notifyListeners();
try {
var res = await HttpUtil().get("/user/list");
data = res["data"];
} catch (e) {
errorMsg = e.toString();
} finally {
loading = false;
notifyListeners();
}
}
}
使用页面
dart
Consumer<ApiStore>(builder: (ctx, store, child) {
if (store.errorMsg != null) return Text(store.errorMsg!);
return ListView.builder(itemCount: store.data.length, itemBuilder: ...);
})
11.8 shared_preferences 简易缓存(轻量替代 Hive)
适合简单字符串缓存,无需复杂对象存储
dart
import 'package:shared_preferences/shared_preferences';
class SimpleCache {
static Future setCache(String key, String value, int expireMin) async {
var sp = await SharedPreferences.getInstance();
int time = DateTime.now().millisecondsSinceEpoch;
await sp.setString("cache_$key", value);
await sp.setInt("cache_time_$key", time);
await sp.setInt("cache_expire_$key", expireMin * 60 * 1000);
}
static Future<String?> getCache(String key) async {
var sp = await SharedPreferences.getInstance();
String? data = sp.getString("cache_$key");
int cacheTime = sp.getInt("cache_time_$key") ?? 0;
int expire = sp.getInt("cache_expire_$key") ?? 0;
int now = DateTime.now().millisecondsSinceEpoch;
if (now - cacheTime > expire) return null;
return data;
}
}
总结
- 页面进入 → Riverpod AsyncNotifier.build 自动发起请求
- HttpUtil 拦截器触发:弹出全局 Loading
- CacheInterceptor 优先读取本地缓存,有缓存直接返回不请求网络
- 无缓存走真实网络,失败触发 RetryInterceptor 自动重试 2 次
- 请求成功:更新 Hive 缓存、关闭 Loading、更新 AsyncData 状态渲染列表
- 请求失败:关闭 Loading、更新 AsyncError 展示错误文案
- 下拉刷新:调用 notifier.refresh (),设置 noCache 强制拉取最新数据
- 页面销毁:CancelToken 取消未完成请求,杜绝内存泄漏