Flutter网络请求进阶拓展:多环境适配、请求重试与断点续传实战
Flutter网络请求进阶拓展:多环境适配、请求重试与断点续传实战
在企业级Flutter应用开发中,除了数据安全、并发控制与异常监控,“多环境灵活切换”“不稳定网络下的请求重试”“大文件传输的断点续传”也是高频核心需求。开发/测试/生产环境的混用易导致数据污染,弱网或临时网络波动易引发请求失败,大文件上传下载中断后重新传输则会浪费带宽与时间。本文将聚焦这三大场景,提供可直接落地的实战方案,进一步完善Flutter网络请求的高级能力体系。
一、核心设计思路:适配业务全流程需求
本次拓展方案遵循三大核心设计思路,确保功能的实用性与可扩展性:
-
环境隔离原则:严格区分开发、测试、预发布、生产等环境,通过配置中心统一管理环境信息,支持动态切换且不影响应用打包,避免硬编码导致的环境混淆。
-
智能重试原则:重试策略需“按需触发”,结合异常类型(如网络中断、超时)、接口类型(如GET可重试,POST需幂等性校验)动态判断,同时通过指数退避算法避免频繁重试加剧服务端压力。
-
高效续传原则:断点续传需基于HTTP Range协议,配合本地文件分片存储与进度记录,确保中断后可从上次进度继续传输,同时支持暂停、恢复、取消操作,提升用户体验。
二、多环境适配:配置中心与动态切换
多环境适配的核心是“配置集中管理+动态切换”,本节实现基于配置文件的环境管理方案,支持打包时指定环境、运行时动态切换(仅开发/测试环境可用),同时隔离各环境的基础URL、加密密钥、日志级别等配置。
1. 基础准备:环境配置模型与配置文件
定义环境枚举与配置模型,通过JSON文件存储各环境配置,实现配置与代码解耦。
// 环境枚举
enum Environment {
dev, // 开发环境
test, // 测试环境
pre, // 预发布环境
prod, // 生产环境
}
// 环境配置模型
class EnvConfig {
// 基础URL
final String baseUrl;
// 签名密钥(与服务端约定)
final String signSecret;
// 是否开启日志打印
final bool enableLog;
// 是否开启加密
final bool enableEncrypt;
// 跳过加密的接口路径
final List<String> skipEncryptPaths;
// 异常上报接口地址
final String exceptionReportUrl;
EnvConfig({
required this.baseUrl,
required this.signSecret,
required this.enableLog,
required this.enableEncrypt,
required this.skipEncryptPaths,
required this.exceptionReportUrl,
});
// 从JSON映射为模型
factory EnvConfig.fromJson(Map<String, dynamic> json) {
return EnvConfig(
baseUrl: json['baseUrl'] as String,
signSecret: json['signSecret'] as String,
enableLog: json['enableLog'] as bool,
enableEncrypt: json['enableEncrypt'] as bool,
skipEncryptPaths: (json['skipEncryptPaths'] as List)
.map((e) => e as String)
.toList(),
exceptionReportUrl: json['exceptionReportUrl'] as String,
);
}
}
在assets/config目录下创建环境配置文件(如env_configs.json),统一管理各环境配置:
{
"dev": {
"baseUrl": "https://dev-api.your-domain.com",
"signSecret": "dev_sign_secret_123",
"enableLog": true,
"enableEncrypt": false,
"skipEncryptPaths": ["/api/login", "/api/register"],
"exceptionReportUrl": "https://dev-monitor.your-domain.com/exception"
},
"test": {
"baseUrl": "https://test-api.your-domain.com",
"signSecret": "test_sign_secret_456",
"enableLog": true,
"enableEncrypt": true,
"skipEncryptPaths": ["/api/login"],
"exceptionReportUrl": "https://test-monitor.your-domain.com/exception"
},
"pre": {
"baseUrl": "https://pre-api.your-domain.com",
"signSecret": "pre_sign_secret_789",
"enableLog": false,
"enableEncrypt": true,
"skipEncryptPaths": [],
"exceptionReportUrl": "https://pre-monitor.your-domain.com/exception"
},
"prod": {
"baseUrl": "https://api.your-domain.com",
"signSecret": "prod_sign_secret_000",
"enableLog": false,
"enableEncrypt": true,
"skipEncryptPaths": [],
"exceptionReportUrl": "https://monitor.your-domain.com/exception"
}
}
2. 实现配置管理工具类:加载与切换环境
封装配置管理工具类,负责加载配置文件、切换环境、提供全局配置访问入口,支持运行时动态切换(开发/测试环境)。
import 'dart:convert';
import 'package:flutter/services.dart';
class EnvManager {
static final EnvManager _instance = EnvManager._internal();
factory EnvManager() => _instance;
EnvManager._internal();
// 当前环境(默认生产环境,避免开发环境配置泄露)
Environment _currentEnv = Environment.prod;
// 所有环境配置
late Map<Environment, EnvConfig> _allConfigs;
// 当前环境配置
EnvConfig get currentConfig => _allConfigs[_currentEnv]!;
// 初始化:加载环境配置文件
Future<void> init() async {
try {
// 加载assets中的配置文件
final configString = await rootBundle.loadString('assets/config/env_configs.json');
final configJson = json.decode(configString) as Map<String, dynamic>;
// 映射为EnvConfig模型
_allConfigs = {
Environment.dev: EnvConfig.fromJson(configJson['dev'] as Map<String, dynamic>),
Environment.test: EnvConfig.fromJson(configJson['test'] as Map<String, dynamic>),
Environment.pre: EnvConfig.fromJson(configJson['pre'] as Map<String, dynamic>),
Environment.prod: EnvConfig.fromJson(configJson['prod'] as Map<String, dynamic>),
};
// (可选)根据打包参数指定初始环境(如flutter run --dart-define=ENV=dev)
final envParam = const String.fromEnvironment('ENV');
if (envParam.isNotEmpty) {
_currentEnv = _parseEnvFromParam(envParam);
}
} catch (e) {
throw Exception('环境配置初始化失败:$e');
}
}
// 动态切换环境(仅开发/测试环境可用,生产环境禁止调用)
void switchEnvironment(Environment env) {
// 生产环境禁止切换
if (_currentEnv == Environment.prod) {
throw Exception('生产环境不支持切换环境');
}
_currentEnv = env;
}
// 从字符串参数解析环境
Environment _parseEnvFromParam(String param) {
switch (param.toLowerCase()) {
case 'dev':
return Environment.dev;
case 'test':
return Environment.test;
case 'pre':
return Environment.pre;
case 'prod':
return Environment.prod;
default:
return Environment.prod;
}
}
}
3. 整合到NetworkUtil:配置动态生效
修改之前的AdvancedNetworkUtil,从EnvManager获取配置,实现环境切换后配置自动生效。
class AdvancedNetworkUtil {
static final AdvancedNetworkUtil _instance = AdvancedNetworkUtil._internal();
factory AdvancedNetworkUtil() => _instance;
late Dio _dio;
late EncryptUtil _encryptUtil;
late ConcurrentControlInterceptor _concurrentInterceptor;
late EnvManager _envManager;
AdvancedNetworkUtil._internal() {
_encryptUtil = EncryptUtil();
_envManager = EnvManager();
// 初始化时加载环境配置
_initEnvAndDio();
}
// 初始化环境与Dio
Future<void> _initEnvAndDio() async {
await _envManager.init();
_initDio();
}
// 重新初始化Dio(环境切换后调用)
void _reinitDio() {
_dio = Dio();
final currentConfig = _envManager.currentConfig;
// 1. 初始化加密工具(从环境配置获取密钥)
_encryptUtil.initRSAKeyPair(
publicKeyStr: '-----BEGIN PUBLIC KEY-----...-----END PUBLIC KEY-----', // 可从环境配置读取
privateKeyStr: '-----BEGIN PRIVATE KEY-----...-----END PRIVATE KEY-----',
);
// 2. 添加异常监控拦截器(使用环境配置的上报地址)
_dio.interceptors.add(
ExceptionMonitorInterceptor(
config: ExceptionMonitorConfig(
enableLocalLog: currentConfig.enableLog,
enableRemoteReport: true,
reportLevelThreshold: ExceptionLevel.warning,
onReport: (exception) async {
await _dio.post(
currentConfig.exceptionReportUrl,
data: exception.toJson(),
options: Options(extra: {'concurrentControl': ConcurrentControlConfig(skipLimit: true)}),
);
},
),
),
);
// 3. 添加并发控制拦截器
_concurrentInterceptor = ConcurrentControlInterceptor(defaultEnableLimit: true);
_dio.interceptors.add(_concurrentInterceptor);
// 4. 添加加密拦截器(使用环境配置的加密开关与密钥)
_dio.interceptors.add(
EncryptInterceptor(
config: EncryptInterceptorConfig(
enableEncrypt: currentConfig.enableEncrypt,
signSecret: currentConfig.signSecret,
skipEncryptPaths: currentConfig.skipEncryptPaths,
),
),
);
// 5. 基础配置(从环境配置获取baseUrl)
_dio.options.baseUrl = currentConfig.baseUrl;
_dio.options.connectTimeout = const Duration(milliseconds: 15000);
}
// 对外提供环境切换方法
Future<void> switchEnv(Environment env) async {
_envManager.switchEnvironment(env);
_reinitDio(); // 重新初始化Dio使配置生效
}
// 其他方法(adjustConcurrentRate、toggleEncrypt、request等)保持不变...
}
三、请求重试:基于指数退避的智能重试策略
请求重试需避免“盲目重试”,本节实现基于Dio拦截器的智能重试方案,支持配置重试次数、重试间隔(指数退避)、可重试的异常类型与接口类型,确保重试的安全性与有效性。
1. 基础准备:重试配置模型
定义重试配置,支持全局默认配置与单个请求的个性化配置。
// 重试配置
class RetryConfig {
// 是否开启重试
final bool enableRetry;
// 最大重试次数
final int maxRetries;
// 初始重试间隔(毫秒)
final int initialDelay;
// 可重试的异常类型
final List<DioExceptionType> retryableExceptionTypes;
// 可重试的HTTP方法(如GET、HEAD,POST需确保幂等性)
final List<String> retryableMethods;
RetryConfig({
this.enableRetry = true,
this.maxRetries = 3,
this.initialDelay = 1000, // 初始1秒
this.retryableExceptionTypes = const [
DioExceptionType.connectionTimeout,
DioExceptionType.sendTimeout,
DioExceptionType.receiveTimeout,
DioExceptionType.connectionError,
],
this.retryableMethods = const ['GET', 'HEAD', 'OPTIONS'],
});
// 复制方法(用于个性化配置)
RetryConfig copyWith({
bool? enableRetry,
int? maxRetries,
int? initialDelay,
List<DioExceptionType>? retryableExceptionTypes,
List<String>? retryableMethods,
}) {
return RetryConfig(
enableRetry: enableRetry ?? this.enableRetry,
maxRetries: maxRetries ?? this.maxRetries,
initialDelay: initialDelay ?? this.initialDelay,
retryableExceptionTypes: retryableExceptionTypes ?? this.retryableExceptionTypes,
retryableMethods: retryableMethods ?? this.retryableMethods,
);
}
}
2. 实现重试拦截器:指数退避算法
通过Dio的onError拦截器捕获异常,判断是否符合重试条件,符合则通过指数退避算法计算重试间隔,触发重试。
import 'dart:async';
import 'package:dio/dio.dart';
class RetryInterceptor extends Interceptor {
// 全局默认重试配置
final RetryConfig defaultRetryConfig;
// 记录当前请求的重试次数
final Map<String, int> _retryCounts = {};
RetryInterceptor({
this.defaultRetryConfig = const RetryConfig(),
});
@override
Future<void> onError(DioException err, ErrorInterceptorHandler handler) async {
// 1. 获取当前请求的重试配置
final requestConfig = err.requestOptions.extra['retryConfig'] as RetryConfig? ?? defaultRetryConfig;
// 2. 检查是否开启重试
if (!requestConfig.enableRetry) {
handler.next(err);
return;
}
// 3. 检查请求方法是否可重试
if (!requestConfig.retryableMethods.contains(err.requestOptions.method.toUpperCase())) {
handler.next(err);
return;
}
// 4. 检查异常类型是否可重试
if (!requestConfig.retryableExceptionTypes.contains(err.type)) {
handler.next(err);
return;
}
// 5. 检查是否超过最大重试次数
final requestKey = '${err.requestOptions.method}-${err.requestOptions.uri}';
final currentRetryCount = _retryCounts[requestKey] ?? 0;
if (currentRetryCount >= requestConfig.maxRetries) {
_retryCounts.remove(requestKey); // 清除重试计数
handler.next(err);
return;
}
// 6. 计算重试间隔(指数退避:initialDelay * 2^currentRetryCount)
final delay = Duration(
milliseconds: requestConfig.initialDelay * (1 << currentRetryCount),
);
// 7. 延迟后重试
await Future.delayed(delay);
_retryCounts[requestKey] = currentRetryCount + 1; // 更新重试计数
try {
// 重新发送请求
final response = await err.requestOptions.requestInterceptor!(err.requestOptions);
_retryCounts.remove(requestKey); // 重试成功,清除计数
handler.resolve(response);
} catch (e) {
// 重试失败,继续触发错误拦截
handler.next(err);
}
}
}
3. 整合到NetworkUtil:支持个性化重试配置
在AdvancedNetworkUtil的_initDio方法中添加重试拦截器,并扩展request方法支持传入个性化重试配置。
void _reinitDio() {
_dio = Dio();
final currentConfig = _envManager.currentConfig;
// ... 其他拦截器(异常监控、并发控制、加密)添加逻辑保持不变
// 添加重试拦截器(放在最后,确保能捕获前面拦截器的异常)
_dio.interceptors.add(RetryInterceptor(
defaultRetryConfig: const RetryConfig(
enableRetry: true,
maxRetries: 3,
initialDelay: 1000,
),
));
// ... 基础配置逻辑保持不变
}
// 扩展request方法,添加retryConfig参数
Future<T?> request<T>(
String path, {
required String method,
Map<String, dynamic>? queryParams,
dynamic data,
Options? options,
ConcurrentControlConfig? concurrentControl,
bool skipEncrypt = false,
RetryConfig? retryConfig, // 新增重试配置参数
}) async {
final extra = <String, dynamic>{};
if (concurrentControl != null) {
extra['concurrentControl'] = concurrentControl;
}
if (skipEncrypt) {
extra['skipEncrypt'] = true;
}
if (retryConfig != null) {
extra['retryConfig'] = retryConfig; // 传入重试配置
}
final requestOptions = Options(
method: method,
...options,
extra: {
...options?.extra ?? {},
...extra,
},
);
// ... 后续请求逻辑保持不变
}
四、断点续传:基于HTTP Range的大文件传输
断点续传适用于大文件(如图片、视频、安装包)的上传/下载场景,本节分别实现断点下载与断点上传功能,基于HTTP Range协议实现进度记录与续传,结合本地文件操作确保数据安全。
1. 基础准备:进度回调与续传配置
定义进度回调函数与续传配置,支持记录传输进度、暂停/恢复/取消操作。
// 传输进度回调(已传输字节数,总字节数)
typedef ProgressCallback = void Function(int sent, int total);
// 续传配置
class ResumeConfig {
// 本地文件路径(下载:保存路径;上传:文件路径)
final String localPath;
// 是否支持续传
final bool enableResume;
// 分片大小(上传时使用,默认5MB)
final int chunkSize;
ResumeConfig({
required this.localPath,
this.enableResume = true,
this.chunkSize = 5 * 1024 * 1024, // 5MB
});
}
// 传输控制器(用于暂停、恢复、取消)
class TransferController {
// 暂停控制器
final Completer<void> _pauseCompleter = Completer<void>();
// 是否取消
bool _isCancelled = false;
// 是否暂停
bool _isPaused = false;
// 暂停
void pause() {
if (_isCancelled) return;
_isPaused = true;
_pauseCompleter.complete();
}
// 恢复
void resume() {
if (_isCancelled) return;
_isPaused = false;
_pauseCompleter = Completer<void>();
}
// 取消
void cancel() {
_isCancelled = true;
_isPaused = false;
if (!_pauseCompleter.isCompleted) {
_pauseCompleter.complete();
}
}
// 等待暂停结束
Future<void> waitForResume() async {
if (_isPaused && !_pauseCompleter.isCompleted) {
await _pauseCompleter.future;
}
}
// 是否取消
bool get isCancelled => _isCancelled;
// 是否暂停
bool get isPaused => _isPaused;
}
2. 实现断点下载:基于HTTP Range
断点下载核心逻辑:检查本地已下载文件大小,通过HTTP Range请求头请求剩余字节,将下载数据追加到本地文件,同时记录下载进度。
import 'dart:io';
import 'package:dio/dio.dart';
class ResumeDownloadUtil {
final Dio _dio;
ResumeDownloadUtil(this._dio);
// 断点下载
Future<void> download({
required String url,
required ResumeConfig config,
required ProgressCallback progressCallback,
required TransferController controller,
}) async {
if (controller.isCancelled) {
throw Exception('下载已取消');
}
final localFile = File(config.localPath);
int downloadedLength = 0;
// 1. 检查本地文件是否存在,获取已下载长度
if (config.enableResume && await localFile.exists()) {
downloadedLength = (await localFile.length()).toInt();
}
// 2. 获取文件总大小(通过HEAD请求)
final headResponse = await _dio.head(url);
final totalLength = int.parse(headResponse.headers.value('content-length') ?? '0');
// 3. 已下载完成,直接返回
if (downloadedLength >= totalLength && totalLength > 0) {
progressCallback(totalLength, totalLength);
return;
}
// 4. 配置Range请求头,请求剩余字节
final options = Options(
headers: {
'Range': 'bytes=$downloadedLength-', // 从已下载位置开始请求
},
responseType: ResponseType.stream, // 流式响应,避免内存占用过大
);
// 5. 发起下载请求
final response = await _dio.get<ResponseBody>(url, options: options);
final stream = response.data?.stream;
if (stream == null) {
throw Exception('下载流获取失败');
}
// 6. 写入本地文件(追加模式)
final fileSink = localFile.openWrite(mode: FileMode.append);
int currentLength = downloadedLength;
await for (final chunk in stream) {
// 检查是否取消或暂停
if (controller.isCancelled) {
await fileSink.close();
throw Exception('下载已取消');
}
await controller.waitForResume();
// 写入数据
await fileSink.add(chunk);
currentLength += chunk.length;
// 回调进度
progressCallback(currentLength, totalLength);
}
await fileSink.close();
// 检查下载是否完整
if (currentLength != totalLength && totalLength > 0) {
throw Exception('下载不完整,已下载:$currentLength,总大小:$totalLength');
}
}
}
3. 实现断点上传:分片上传+续传
断点上传核心逻辑:将文件分片,记录已上传分片索引,续传时仅上传未完成的分片,最后通知服务端合并分片。
import 'dart:io';
import 'dart:convert';
import 'package:dio/dio.dart';
class ResumeUploadUtil {
final Dio _dio;
ResumeUploadUtil(this._dio);
// 断点上传(基于分片上传)
Future<void> upload({
required String url,
required ResumeConfig config,
required ProgressCallback progressCallback,
required TransferController controller,
required String fileId, // 文件唯一标识(用于服务端识别文件)
}) async {
if (controller.isCancelled) {
throw Exception('上传已取消');
}
final localFile = File(config.localPath);
if (!await localFile.exists()) {
throw Exception('本地文件不存在:${config.localPath}');
}
// 1. 获取文件信息
final fileLength = (await localFile.length()).toInt();
final chunkSize = config.chunkSize;
final totalChunks = (fileLength / chunkSize).ceil(); // 总分片数
// 2. 检查已上传分片(从服务端获取已上传分片索引)
final uploadedChunks = await _getUploadedChunks(url, fileId);
int uploadedLength = uploadedChunks.length * chunkSize;
// 3. 上传进度回调(初始进度)
progressCallback(uploadedLength, fileLength);
// 4. 遍历分片上传
for (int i = 0; i < totalChunks; i++) {
// 跳过已上传分片
if (uploadedChunks.contains(i)) {
continue;
}
// 检查是否取消或暂停
if (controller.isCancelled) {
throw Exception('上传已取消');
}
await controller.waitForResume();
// 计算当前分片的起始和结束位置
final start = i * chunkSize;
final end = (i + 1) * chunkSize > fileLength ? fileLength : (i + 1) * chunkSize;
final chunk = await localFile.readAsBytes(start: start, end: end);
// 5. 上传当前分片
await _uploadChunk(
url: url,
fileId: fileId,
chunkIndex: i,
totalChunks: totalChunks,
chunk: chunk,
);
// 6. 更新上传进度
uploadedLength += chunk.length;
progressCallback(uploadedLength, fileLength);
}
// 7. 通知服务端合并分片
await _mergeChunks(url, fileId, totalChunks);
}
// 从服务端获取已上传分片索引
Future<List<int>> _getUploadedChunks(String url, String fileId) async {
try {
final response = await _dio.get(
'$url/check',
queryParameters: {'fileId': fileId},
);
final data = response.data as Map<String, dynamic>;
return (data['uploadedChunks'] as List).map((e) => e as int).toList();
} catch (e) {
// 若查询失败,默认重新上传所有分片
return [];
}
}
// 上传单个分片
Future<void> _uploadChunk({
required String url,
required String fileId,
required int chunkIndex,
required int totalChunks,
required List<int> chunk,
}) async {
final formData = FormData.fromMap({
'fileId': fileId,
'chunkIndex': chunkIndex,
'totalChunks': totalChunks,
'chunk': MultipartFile.fromBytes(
chunk,
filename: 'chunk_$chunkIndex',
),
});
await _dio.post(
'$url/uploadChunk',
data: formData,
);
}
// 通知服务端合并分片
Future<void> _mergeChunks(String url, String fileId, int totalChunks) async {
await _dio.post(
'$url/merge',
data: {
'fileId': fileId,
'totalChunks': totalChunks,
},
);
}
}
4. 整合到NetworkUtil:提供统一调用入口
class AdvancedNetworkUtil {
// ... 其他属性与方法保持不变
// 初始化续传工具
late ResumeDownloadUtil _resumeDownloadUtil;
late ResumeUploadUtil _resumeUploadUtil;
AdvancedNetworkUtil._internal() {
_encryptUtil = EncryptUtil();
_envManager = EnvManager();
_resumeDownloadUtil = ResumeDownloadUtil(_dio);
_resumeUploadUtil = ResumeUploadUtil(_dio);
_initEnvAndDio();
}
// 重新初始化时更新续传工具的Dio实例
void _reinitDio() {
_dio = Dio();
_resumeDownloadUtil = ResumeDownloadUtil(_dio);
_resumeUploadUtil = ResumeUploadUtil(_dio);
// ... 其他初始化逻辑保持不变
}
// 断点下载入口
Future<void> resumeDownload({
required String url,
required ResumeConfig config,
required ProgressCallback progressCallback,
required TransferController controller,
}) =>
_resumeDownloadUtil.download(
url: url,
config: config,
progressCallback: progressCallback,
controller: controller,
);
// 断点上传入口
Future<void> resumeUpload({
required String url,
required ResumeConfig config,
required ProgressCallback progressCallback,
required TransferController controller,
required String fileId,
}) =>
_resumeUploadUtil.upload(
url: url,
config: config,
progressCallback: progressCallback,
controller: controller,
fileId: fileId,
);
}
五、结语:构建全场景覆盖的网络请求体系
本文补充的“多环境适配、请求重试、断点续传”三大核心功能,与此前的“加密、并发控制、异常监控”形成完整的Flutter网络请求高级体系,覆盖了企业级应用从开发调试到生产运行、从普通接口请求到大型文件传输的全场景需求。
实际应用中,需结合业务特性灵活调整配置:例如金融类应用可强化加密与环境隔离,社交类应用可优化重试策略与断点续传体验,工具类应用可精简配置提升性能。通过本文的实战方案,开发者可快速搭建稳定、高效、安全的网络交互层,减少重复开发工作,聚焦核心业务逻辑实现。同时,建议在实际项目中持续迭代优化,例如添加配置中心远程拉取配置、基于网络质量动态调整重试与并发参数等,进一步提升网络请求
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
更多推荐




所有评论(0)