精通 Flutter 网络请求:从基础 GET/POST 到拦截器 + 缓存 + 断点续传的全维度实践
基础请求:封装全局 Dio 实例,统一处理请求参数和数据解析;拦截器:实现请求头自动添加、响应统一解析、Token 过期处理;缓存策略:减少重复请求,提升页面加载速度;高级功能:断点续传下载、文件上传、取消请求等,适配复杂业务场景。网络请求是 Flutter 应用的 “生命线”,写得规范与否直接影响应用的稳定性和用户体验。比如统一的异常处理能避免崩溃,缓存策略能提升加载速度,断点续传能优化大文件下
欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。
网络请求是 Flutter 应用与后端交互的 “桥梁”—— 登录、数据展示、文件上传下载等核心功能都离不开网络请求。但很多开发者仅停留在 “能请求” 的层面,忽略了异常处理、请求拦截、数据缓存、断点续传等关键场景。本文将从基础的 GET/POST 请求入手,逐步实现拦截器、请求缓存、文件断点续传等进阶功能,既有严谨的代码规范,又有生动的场景拆解,让你彻底掌握 Flutter 网络请求的精髓。
一、Flutter 网络请求核心认知:为什么选择 Dio?
先理清 Flutter 网络请求的核心逻辑,明确技术选型的底层逻辑:
- 原生 HttpClient 的痛点:原生
HttpClientAPI 繁琐,不支持拦截器、FormData、文件上传等高级功能,需手动处理序列化 / 反序列化; - Dio 的优势:Dio 是 Flutter 生态中最主流的网络请求库,支持拦截器、FormData、文件上传下载、超时控制、取消请求等,API 简洁且扩展性强;
- 核心设计原则:网络请求需遵循 “异常兜底、数据解析、状态管理” 三位一体的原则,避免崩溃和数据错乱。
本文所有代码基于:
plaintext
Flutter 3.26.0
Dart 3.6.0
dio: ^5.5.0
shared_preferences: ^2.2.2 # 缓存存储
path_provider: ^2.1.2 # 本地文件路径
二、入门:基础 GET/POST 请求 + 数据解析
先实现最基础的 GET/POST 请求,掌握 Dio 的核心用法和 JSON 数据解析,这是网络请求的基础模板。
2.1 第一步:封装基础 Dio 实例
创建utils/dio_client.dart,封装全局 Dio 实例,统一配置基础参数:
dart
import 'package:dio/dio.dart';
// 全局Dio实例封装
class DioClient {
static late Dio _dio;
// 初始化Dio配置
static void init() {
_dio = Dio(
BaseOptions(
// 基础URL(实际开发中替换为真实接口地址)
baseUrl: 'https://api.example.com/v1',
// 连接超时时间
connectTimeout: const Duration(seconds: 5),
// 接收超时时间
receiveTimeout: const Duration(seconds: 3),
// 响应数据类型
responseType: ResponseType.json,
// 请求头
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
}
// 获取Dio实例
static Dio get instance => _dio;
// 简化GET请求
static Future<T> get<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
final response = await _dio.get(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
return response.data as T;
} on DioException catch (e) {
throw _handleDioError(e);
}
}
// 简化POST请求
static Future<T> post<T>(
String path, {
dynamic data,
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
}) async {
try {
final response = await _dio.post(
path,
data: data,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
return response.data as T;
} on DioException catch (e) {
throw _handleDioError(e);
}
}
// 统一异常处理
static String _handleDioError(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
return '网络连接超时,请检查网络';
case DioExceptionType.receiveTimeout:
return '数据接收超时,请稍后重试';
case DioExceptionType.badResponse:
return '接口返回错误:${e.response?.statusCode}';
case DioExceptionType.cancel:
return '请求已取消';
case DioExceptionType.connectionError:
return '网络连接失败,请检查网络';
default:
return '请求失败:${e.message ?? '未知错误'}';
}
}
}
代码解析:
- 全局单例 Dio:通过静态方法
init初始化 Dio,避免重复创建实例; - BaseOptions 配置:统一设置基础 URL、超时时间、请求头等,减少重复代码;
- 简化 GET/POST 方法:封装通用请求方法,自动处理异常并抛出友好提示;
- 统一异常处理:将 Dio 的各类异常转换为用户友好的提示文字,避免直接抛出技术异常。
2.2 第二步:初始化 Dio 并实现基础请求
修改main.dart,在应用启动时初始化 Dio:
dart
import 'package:flutter/material.dart';
import 'utils/dio_client.dart';
import 'pages/network_demo_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 初始化Dio
DioClient.init();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter网络请求实战',
theme: ThemeData(primarySwatch: Colors.blue),
home: const NetworkDemoPage(),
);
}
}
2.3 第三步:实现 GET/POST 请求与数据解析
创建pages/network_demo_page.dart,实现用户列表 GET 请求和用户创建 POST 请求:
dart
import 'package:flutter/material.dart';
import 'utils/dio_client.dart';
// 数据模型:用户
class User {
final int id;
final String name;
final String email;
User({required this.id, required this.name, required this.email});
// 从JSON解析模型
factory User.fromJson(Map<String, dynamic> json) {
return User(
id: json['id'],
name: json['name'],
email: json['email'],
);
}
// 转换为JSON
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
};
}
}
class NetworkDemoPage extends StatefulWidget {
const NetworkDemoPage({super.key});
@override
State<NetworkDemoPage> createState() => _NetworkDemoPageState();
}
class _NetworkDemoPageState extends State<NetworkDemoPage> {
// 加载状态
bool _isLoading = false;
// 用户列表
List<User> _userList = [];
// 错误提示
String? _errorMsg;
// 获取用户列表(GET请求)
Future<void> _fetchUserList() async {
setState(() {
_isLoading = true;
_errorMsg = null;
});
try {
// 发起GET请求(模拟接口:/users)
final response = await DioClient.get<Map<String, dynamic>>(
'/users',
queryParameters: {'page': 1, 'size': 10}, // 请求参数
);
// 解析JSON数据
final List<dynamic> dataList = response['data'];
setState(() {
_userList = dataList.map((json) => User.fromJson(json)).toList();
});
} catch (e) {
setState(() {
_errorMsg = e.toString();
});
} finally {
setState(() {
_isLoading = false;
});
}
}
// 创建用户(POST请求)
Future<void> _createUser() async {
setState(() {
_isLoading = true;
_errorMsg = null;
});
try {
// 构建请求体
final userData = User(
id: 0, // 后端生成ID
name: 'Flutter测试用户',
email: 'flutter_test@example.com',
).toJson();
// 发起POST请求
await DioClient.post(
'/users',
data: userData,
);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('用户创建成功!')),
);
// 重新获取用户列表
_fetchUserList();
}
} catch (e) {
setState(() {
_errorMsg = e.toString();
});
} finally {
setState(() {
_isLoading = false;
});
}
}
@override
void initState() {
super.initState();
// 页面加载时获取用户列表
_fetchUserList();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('基础网络请求')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// 操作按钮
Row(
children: [
ElevatedButton(
onPressed: _isLoading ? null : _fetchUserList,
child: const Text('刷新用户列表'),
),
const SizedBox(width: 16),
ElevatedButton(
onPressed: _isLoading ? null : _createUser,
child: const Text('创建测试用户'),
),
],
),
const SizedBox(height: 20),
// 加载状态/错误提示/用户列表
_isLoading
? const Center(child: CircularProgressIndicator())
: _errorMsg != null
? Center(
child: Text(
_errorMsg!,
style: const TextStyle(color: Colors.red, fontSize: 16),
),
)
: Expanded(
child: ListView.builder(
itemCount: _userList.length,
itemBuilder: (context, index) {
final user = _userList[index];
return ListTile(
key: ValueKey(user.id),
title: Text(user.name),
subtitle: Text(user.email),
);
},
),
),
],
),
),
);
}
}
核心解析:
-
数据模型设计:
- 定义
User模型类,通过fromJson/toJson方法实现 JSON 与模型的互转,避免直接操作 Map,提升代码可读性; - 模型类的字段与接口返回字段一一对应,减少解析错误。
- 定义
-
请求状态管理:
- 通过
_isLoading控制加载状态,避免重复请求; _errorMsg存储错误提示,友好展示给用户;- 所有异步操作包裹在
try-catch中,防止崩溃。
- 通过
-
请求参数处理:
- GET 请求通过
queryParameters传递 URL 参数; - POST 请求通过
data传递 JSON 请求体; - 所有请求都关联页面生命周期,通过
mounted判断页面是否挂载。
- GET 请求通过
三、进阶:拦截器实现 + 请求 / 响应统一处理
拦截器是 Dio 的核心特性,可实现请求头自动添加、响应数据统一解析、Token 过期自动刷新等功能,是企业级开发的必备技能。
3.1 实现通用拦截器
修改utils/dio_client.dart,添加请求拦截器和响应拦截器:
dart
import 'package:dio/dio.dart';
class DioClient {
static late Dio _dio;
static void init() {
_dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com/v1',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
responseType: ResponseType.json,
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
// 添加请求拦截器
_dio.interceptors.add(
InterceptorsWrapper(
// 请求发送前拦截
onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
// 1. 自动添加Token(实际开发中从本地存储获取)
final token = 'your_token_here';
if (token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
// 2. 打印请求日志(调试用)
debugPrint('请求URL:${options.uri}');
debugPrint('请求参数:${options.data}');
// 继续执行请求
handler.next(options);
},
// 响应返回后拦截
onResponse: (Response response, ResponseInterceptorHandler handler) {
// 1. 统一解析响应数据(假设接口返回格式:{code: 200, data: ..., msg: ''})
final Map<String, dynamic> responseData = response.data;
if (responseData['code'] == 200) {
// 只返回data部分,简化上层解析
response.data = responseData['data'];
} else {
// 非200状态码抛出异常
handler.reject(
DioException(
requestOptions: response.requestOptions,
response: response,
type: DioExceptionType.badResponse,
message: responseData['msg'] ?? '接口返回错误',
),
true,
);
return;
}
// 2. 打印响应日志
debugPrint('响应数据:${response.data}');
// 继续处理响应
handler.next(response);
},
// 请求失败时拦截
onError: (DioException e, ErrorInterceptorHandler handler) {
// 1. Token过期自动刷新(示例逻辑)
if (e.response?.statusCode == 401) {
// 这里可实现Token刷新逻辑,刷新后重新发起请求
debugPrint('Token过期,准备刷新');
// 简化处理:直接抛出未登录异常
handler.reject(
DioException(
requestOptions: e.requestOptions,
message: '登录状态已过期,请重新登录',
type: DioExceptionType.badResponse,
),
true,
);
return;
}
// 2. 统一错误处理
handler.next(e);
},
),
);
}
// 其余代码不变...
}
拦截器核心逻辑解析:
-
请求拦截器(onRequest):
- 自动添加 Authorization 请求头,无需在每个请求中手动设置;
- 打印请求日志,便于调试;
- 可扩展:添加请求加密、参数签名等逻辑。
-
响应拦截器(onResponse):
- 统一解析接口返回格式,假设接口返回
{code: 200, data: ..., msg: ''},只向上层返回data部分,简化解析逻辑; - 非 200 状态码直接抛出异常,上层只需处理业务逻辑。
- 统一解析接口返回格式,假设接口返回
-
错误拦截器(onError):
- 捕获 401 状态码,实现 Token 过期自动刷新(示例中简化为提示重新登录);
- 可扩展:添加重试机制、错误日志上报等。
3.2 拦截器使用效果
添加拦截器后,上层代码无需关心:
- Token 的添加:拦截器自动注入;
- 响应数据的统一解析:直接获取
data部分; - Token 过期处理:拦截器统一捕获并提示。
四、高阶 1:请求缓存实现(避免重复请求)
实际开发中,对于不常变化的数据(如首页分类、用户信息),可添加缓存逻辑,减少网络请求,提升页面加载速度。
4.1 封装缓存工具类
创建utils/cache_manager.dart:
dart
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
// 缓存管理工具
class CacheManager {
static late SharedPreferences _prefs;
// 初始化
static Future<void> init() async {
_prefs = await SharedPreferences.getInstance();
}
// 保存缓存
static Future<void> setCache(String key, dynamic data, {Duration? expireTime}) {
final cacheData = {
'data': data,
'timestamp': DateTime.now().millisecondsSinceEpoch,
'expireTime': expireTime?.inMilliseconds ?? 0,
};
return _prefs.setString(key, json.encode(cacheData));
}
// 获取缓存
static dynamic getCache(String key) {
final cacheStr = _prefs.getString(key);
if (cacheStr == null) return null;
final cacheData = json.decode(cacheStr);
final timestamp = cacheData['timestamp'] as int;
final expireTime = cacheData['expireTime'] as int;
// 判断是否过期
if (expireTime > 0) {
final now = DateTime.now().millisecondsSinceEpoch;
if (now - timestamp > expireTime) {
// 过期则删除缓存
removeCache(key);
return null;
}
}
return cacheData['data'];
}
// 删除缓存
static Future<void> removeCache(String key) {
return _prefs.remove(key);
}
// 清空所有缓存
static Future<void> clearAllCache() {
return _prefs.clear();
}
}
4.2 实现带缓存的 GET 请求
修改utils/dio_client.dart,添加带缓存的 GET 请求方法:
dart
// 带缓存的GET请求
static Future<T> getWithCache<T>(
String path, {
Map<String, dynamic>? queryParameters,
Options? options,
CancelToken? cancelToken,
Duration? cacheDuration = const Duration(minutes: 10), // 默认缓存10分钟
}) async {
// 构建缓存key
final cacheKey = '$path-${json.encode(queryParameters ?? {})}';
// 先从缓存获取
final cacheData = CacheManager.getCache(cacheKey);
if (cacheData != null) {
return cacheData as T;
}
// 缓存未命中,发起网络请求
try {
final response = await _dio.get(
path,
queryParameters: queryParameters,
options: options,
cancelToken: cancelToken,
);
// 保存缓存
await CacheManager.setCache(cacheKey, response.data, expireTime: cacheDuration);
return response.data as T;
} on DioException catch (e) {
throw _handleDioError(e);
}
}
4.3 使用带缓存的请求
在NetworkDemoPage中替换_fetchUserList方法:
dart
Future<void> _fetchUserList() async {
setState(() {
_isLoading = true;
_errorMsg = null;
});
try {
// 使用带缓存的GET请求
final response = await DioClient.getWithCache<Map<String, dynamic>>(
'/users',
queryParameters: {'page': 1, 'size': 10},
cacheDuration: const Duration(minutes: 5), // 缓存5分钟
);
final List<dynamic> dataList = response['data'];
setState(() {
_userList = dataList.map((json) => User.fromJson(json)).toList();
});
} catch (e) {
setState(() {
_errorMsg = e.toString();
});
} finally {
setState(() {
_isLoading = false;
});
}
}
缓存逻辑解析:
- 缓存 Key 由请求路径 + 参数组成,避免不同参数的请求缓存冲突;
- 优先读取缓存,缓存未命中或过期时才发起网络请求;
- 缓存时长可自定义,灵活适配不同业务场景。
五、高阶 2:文件断点续传下载
文件下载是高频场景,断点续传能避免网络中断后重新下载,提升用户体验。
5.1 封装断点续传下载工具
创建utils/download_manager.dart:
dart
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'dart:io';
// 下载进度回调
typedef DownloadProgressCallback = void Function(int count, int total);
// 断点续传下载管理器
class DownloadManager {
// 开始下载
static Future<void> downloadFile(
String url,
String saveFileName, {
required DownloadProgressCallback onProgress,
required Function(String path) onSuccess,
required Function(String error) onError,
}) async {
try {
// 获取本地存储路径
final directory = await getExternalStorageDirectory();
if (directory == null) {
onError('无法获取存储路径');
return;
}
final savePath = '${directory.path}/$saveFileName';
final file = File(savePath);
int start = 0;
// 如果文件已存在,获取文件大小作为起始位置
if (file.existsSync()) {
start = file.lengthSync();
}
// 发起断点续传请求
final dio = Dio();
await dio.download(
url,
savePath,
options: Options(
headers: {
'Range': 'bytes=$start-', // 断点续传核心:指定起始字节
},
),
onReceiveProgress: onProgress,
// 追加写入文件
deleteOnError: false,
);
onSuccess(savePath);
} catch (e) {
onError(e.toString());
}
}
}
5.2 实现文件下载页面
创建pages/download_page.dart:
dart
import 'package:flutter/material.dart';
import 'utils/download_manager.dart';
class DownloadPage extends StatefulWidget {
const DownloadPage({super.key});
@override
State<DownloadPage> createState() => _DownloadPageState();
}
class _DownloadPageState extends State<DownloadPage> {
// 下载进度
double _progress = 0.0;
// 下载状态
bool _isDownloading = false;
// 下载提示
String _downloadMsg = '未开始下载';
// 开始下载
void _startDownload() {
setState(() {
_isDownloading = true;
_progress = 0.0;
_downloadMsg = '下载中...';
});
// 模拟下载地址(实际替换为真实文件地址)
const downloadUrl = 'https://example.com/file/test.pdf';
const fileName = 'test.pdf';
DownloadManager.downloadFile(
downloadUrl,
fileName,
onProgress: (count, total) {
if (total <= 0) return;
setState(() {
_progress = count / total;
_downloadMsg = '下载中:${(count / total * 100).toStringAsFixed(1)}%';
});
},
onSuccess: (path) {
setState(() {
_isDownloading = false;
_downloadMsg = '下载完成:$path';
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('文件下载成功!')),
);
},
onError: (error) {
setState(() {
_isDownloading = false;
_downloadMsg = '下载失败:$error';
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('下载失败:$error')),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('断点续传下载')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 下载进度条
LinearProgressIndicator(
value: _progress,
minHeight: 10,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
),
const SizedBox(height: 20),
// 下载状态提示
Text(
_downloadMsg,
style: const TextStyle(fontSize: 16),
),
const SizedBox(height: 40),
// 下载按钮
ElevatedButton(
onPressed: _isDownloading ? null : _startDownload,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
),
child: const Text('开始下载文件'),
),
],
),
),
);
}
}
断点续传核心解析:
Range请求头:指定bytes=$start-,告诉服务器从指定字节开始传输,实现断点续传;- 文件追加写入:
deleteOnError: false确保下载中断后文件不被删除,下次可继续下载; - 进度回调:
onReceiveProgress实时获取下载进度,更新 UI 展示。
六、网络请求避坑指南
- 异常处理:
- 所有网络请求必须包裹
try-catch,避免未捕获异常导致崩溃; - 区分网络异常、接口异常、业务异常,分别给出友好提示;
- 所有网络请求必须包裹
- 内存泄漏:
- 使用
CancelToken取消未完成的请求(如页面销毁时); - 异步回调中必须判断
mounted,避免操作已销毁的页面;
- 使用
- 缓存策略:
- 缓存只适用于非敏感、不常变化的数据(如分类、配置);
- 敏感数据(如用户信息)禁止缓存,或设置极短的缓存时长;
- 断点续传:
- 确保服务器支持
Range请求头,否则断点续传无效; - 大文件下载建议分块下载,避免内存溢出;
- 确保服务器支持
- 调试技巧:
- 使用 Dio 的日志拦截器(
LogInterceptor)打印完整的请求 / 响应日志; - 测试时模拟各种异常场景(断网、超时、401/404/500)。
- 使用 Dio 的日志拦截器(
七、总结
Flutter 网络请求的学习路径是 “基础请求→拦截器→缓存→高级功能”,核心原则是 “统一封装、异常兜底、体验优先”:
- 基础请求:封装全局 Dio 实例,统一处理请求参数和数据解析;
- 拦截器:实现请求头自动添加、响应统一解析、Token 过期处理;
- 缓存策略:减少重复请求,提升页面加载速度;
- 高级功能:断点续传下载、文件上传、取消请求等,适配复杂业务场景。
网络请求是 Flutter 应用的 “生命线”,写得规范与否直接影响应用的稳定性和用户体验。比如统一的异常处理能避免崩溃,缓存策略能提升加载速度,断点续传能优化大文件下载体验。希望本文的实战案例和原理解析,能让你避开网络请求的 “坑”,写出既严谨又高性能的 Flutter 网络请求代码。
更多推荐


所有评论(0)