Flutter dio插件的文件上传与下载:实现复杂网络操作
2024-07-291.6k 阅读
Flutter dio插件的文件上传与下载:实现复杂网络操作
一、Flutter dio插件概述
在Flutter开发中,网络请求是非常常见的需求。Dio是一个强大的HTTP请求库,它在Flutter生态系统中被广泛使用。Dio具有以下特点:
- 支持HTTP/HTTPS:能够处理标准的HTTP和加密的HTTPS请求,满足各种网络场景。
- 拦截器:通过拦截器可以方便地对请求和响应进行统一处理,例如添加通用的请求头、处理响应数据的格式等。
- 请求取消:可以在需要的时候取消正在进行的请求,避免资源浪费和潜在的内存泄漏。
- 文件上传与下载:这正是本文重点关注的功能,Dio提供了简洁而高效的API来实现文件的上传和下载操作。
要使用Dio插件,首先需要在pubspec.yaml
文件中添加依赖:
dependencies:
dio: ^4.0.0
然后运行flutter pub get
命令来获取依赖。
二、文件下载
- 简单文件下载 以下是使用Dio进行简单文件下载的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
class FileDownloadPage extends StatefulWidget {
@override
_FileDownloadPageState createState() => _FileDownloadPageState();
}
class _FileDownloadPageState extends State<FileDownloadPage> {
double _progress = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('文件下载'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('下载进度: ${(_progress * 100).toStringAsFixed(2)}%'),
ElevatedButton(
onPressed: () async {
final dio = Dio();
final url = 'http://example.com/file.zip';
final savePath = '${(await getApplicationDocumentsDirectory()).path}/file.zip';
try {
await dio.download(
url,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
setState(() {
_progress = received / total;
});
}
},
);
print('文件下载完成');
} catch (e) {
print('文件下载失败: $e');
}
},
child: Text('开始下载'),
),
],
),
);
}
}
在上述代码中:
- 首先创建了一个
Dio
实例。 - 定义了要下载的文件的URL和保存路径。这里使用
getApplicationDocumentsDirectory
获取应用的文档目录来保存文件。 dio.download
方法执行下载操作,onReceiveProgress
回调用于更新下载进度,received
表示已接收的字节数,total
表示文件总字节数。如果total
为 -1,说明无法获取文件总大小。
- 断点续传
断点续传在网络不稳定或文件较大的情况下非常有用。Dio支持通过设置
Range
请求头来实现断点续传。以下是实现断点续传的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
class ResumeDownloadPage extends StatefulWidget {
@override
_ResumeDownloadPageState createState() => _ResumeDownloadPageState();
}
class _ResumeDownloadPageState extends State<ResumeDownloadPage> {
double _progress = 0;
bool _isDownloading = false;
File? _downloadedFile;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('断点续传'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (_downloadedFile != null)
Text('文件已下载: ${_downloadedFile!.path}'),
Text('下载进度: ${(_progress * 100).toStringAsFixed(2)}%'),
if (_isDownloading)
CircularProgressIndicator(),
ElevatedButton(
onPressed: () async {
if (_isDownloading) {
return;
}
setState(() {
_isDownloading = true;
});
final dio = Dio();
final url = 'http://example.com/large_file.zip';
final savePath = '${(await getApplicationDocumentsDirectory()).path}/large_file.zip';
File file = File(savePath);
int downloadedLength = 0;
if (file.existsSync()) {
downloadedLength = file.lengthSync();
}
try {
await dio.download(
url,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
setState(() {
_progress = (downloadedLength + received) / total;
});
}
},
options: Options(
headers: {
'Range': 'bytes=$downloadedLength-'
}
)
);
setState(() {
_downloadedFile = file;
_isDownloading = false;
});
print('文件下载完成');
} catch (e) {
setState(() {
_isDownloading = false;
});
print('文件下载失败: $e');
}
},
child: Text(_isDownloading? '暂停下载' : '开始下载'),
),
],
),
);
}
}
在这段代码中:
- 首先检查本地是否已经存在部分下载的文件,如果存在,则获取已下载的长度
downloadedLength
。 - 在
dio.download
的options
中设置Range
请求头,格式为bytes=已下载长度-
,表示从已下载的位置继续下载。 - 下载过程中,根据已下载长度和新接收的长度来更新进度。
三、文件上传
- 单文件上传 以下是使用Dio进行单文件上传的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class SingleFileUploadPage extends StatefulWidget {
@override
_SingleFileUploadPageState createState() => _SingleFileUploadPageState();
}
class _SingleFileUploadPageState extends State<SingleFileUploadPage> {
String _uploadStatus = '等待上传';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('单文件上传'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_uploadStatus),
ElevatedButton(
onPressed: () async {
setState(() {
_uploadStatus = '正在上传';
});
final dio = Dio();
final url = 'http://example.com/upload';
final file = File('${(await getTemporaryDirectory()).path}/test.txt');
await file.writeAsString('这是一个测试文件');
FormData formData = FormData.fromMap({
'file': await MultipartFile.fromFile(file.path, filename: 'test.txt')
});
try {
Response response = await dio.post(url, data: formData);
setState(() {
_uploadStatus = '上传成功: ${response.data}';
});
print('文件上传成功: ${response.data}');
} catch (e) {
setState(() {
_uploadStatus = '上传失败: $e';
});
print('文件上传失败: $e');
}
},
child: Text('开始上传'),
),
],
),
);
}
}
在上述代码中:
- 创建了一个临时文件
test.txt
并写入一些测试数据。 - 使用
FormData
来包装要上传的文件,MultipartFile.fromFile
方法用于将本地文件转换为MultipartFile
,并可以指定文件名。 - 通过
dio.post
方法将FormData
发送到服务器。
- 多文件上传
多文件上传与单文件上传类似,只是
FormData
中包含多个MultipartFile
。以下是多文件上传的代码示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class MultipleFileUploadPage extends StatefulWidget {
@override
_MultipleFileUploadPageState createState() => _MultipleFileUploadPageState();
}
class _MultipleFileUploadPageState extends State<MultipleFileUploadPage> {
String _uploadStatus = '等待上传';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('多文件上传'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(_uploadStatus),
ElevatedButton(
onPressed: () async {
setState(() {
_uploadStatus = '正在上传';
});
final dio = Dio();
final url = 'http://example.com/upload_multiple';
List<MultipartFile> files = [];
for (int i = 0; i < 3; i++) {
final file = File('${(await getTemporaryDirectory()).path}/test$i.txt');
await file.writeAsString('这是测试文件 $i');
files.add(await MultipartFile.fromFile(file.path, filename: 'test$i.txt'));
}
FormData formData = FormData.fromMap({
'files': files
});
try {
Response response = await dio.post(url, data: formData);
setState(() {
_uploadStatus = '上传成功: ${response.data}';
});
print('文件上传成功: ${response.data}');
} catch (e) {
setState(() {
_uploadStatus = '上传失败: $e';
});
print('文件上传失败: $e');
}
},
child: Text('开始上传'),
),
],
),
);
}
}
在这段代码中:
- 循环创建了三个临时文件,并将它们转换为
MultipartFile
添加到List
中。 FormData
的files
字段包含了多个MultipartFile
,通过dio.post
发送到服务器进行多文件上传。
四、处理复杂网络操作
- 并发请求 在实际应用中,可能需要同时发起多个网络请求。Dio可以很方便地实现并发请求。以下是一个并发下载多个文件的示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class ConcurrentDownloadPage extends StatefulWidget {
@override
_ConcurrentDownloadPageState createState() => _ConcurrentDownloadPageState();
}
class _ConcurrentDownloadPageState extends State<ConcurrentDownloadPage> {
List<double> _progresses = [];
@override
void initState() {
super.initState();
_progresses = List.filled(3, 0);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('并发下载'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (int i = 0; i < _progresses.length; i++)
Text('文件 $i 下载进度: ${(_progresses[i] * 100).toStringAsFixed(2)}%'),
ElevatedButton(
onPressed: () async {
final dio = Dio();
List<Future> downloadFutures = [];
List<String> urls = [
'http://example.com/file1.zip',
'http://example.com/file2.zip',
'http://example.com/file3.zip'
];
for (int i = 0; i < urls.length; i++) {
final savePath = '${(await getApplicationDocumentsDirectory()).path}/file$i.zip';
downloadFutures.add(dio.download(
urls[i],
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
setState(() {
_progresses[i] = received / total;
});
}
},
));
}
await Future.wait(downloadFutures);
print('所有文件下载完成');
},
child: Text('开始并发下载'),
),
],
),
);
}
}
在上述代码中:
- 创建了一个
List
来存储每个文件的下载进度。 - 遍历URL列表,为每个文件的下载创建一个
Future
并添加到downloadFutures
列表中。 - 使用
Future.wait
等待所有下载任务完成。
- 请求重试机制 网络请求可能会因为各种原因失败,例如网络波动、服务器繁忙等。添加请求重试机制可以提高应用的稳定性。以下是一个带有重试机制的文件下载示例:
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'dart:io';
class RetryDownloadPage extends StatefulWidget {
@override
_RetryDownloadPageState createState() => _RetryDownloadPageState();
}
class _RetryDownloadPageState extends State<RetryDownloadPage> {
double _progress = 0;
int _retryCount = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('重试下载'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('下载进度: ${(_progress * 100).toStringAsFixed(2)}%'),
Text('重试次数: $_retryCount'),
ElevatedButton(
onPressed: () async {
final dio = Dio();
final url = 'http://example.com/file.zip';
final savePath = '${(await getApplicationDocumentsDirectory()).path}/file.zip';
int maxRetries = 3;
while (_retryCount < maxRetries) {
try {
await dio.download(
url,
savePath,
onReceiveProgress: (received, total) {
if (total != -1) {
setState(() {
_progress = received / total;
});
}
},
);
print('文件下载完成');
break;
} catch (e) {
setState(() {
_retryCount++;
});
print('下载失败,重试 ($_retryCount/$maxRetries): $e');
await Future.delayed(Duration(seconds: 2));
}
}
if (_retryCount >= maxRetries) {
print('达到最大重试次数,下载失败');
}
},
child: Text('开始下载'),
),
],
),
);
}
}
在这段代码中:
- 设置了最大重试次数
maxRetries
为3。 - 使用
while
循环进行重试,每次下载失败时,增加重试次数并等待2秒后再次尝试,直到下载成功或达到最大重试次数。
五、错误处理与优化
- 错误处理
在文件上传和下载过程中,可能会遇到各种错误。常见的错误包括网络连接失败、服务器响应错误、文件读写错误等。
- 网络连接失败:这通常是由于网络不可用或服务器无法访问导致的。在Dio中,这类错误通常会抛出
DioError
,并且errorType
为DioErrorType.connectTimeout
或DioErrorType.receiveTimeout
等。可以通过捕获DioError
并根据errorType
进行相应处理。 - 服务器响应错误:例如服务器返回404(未找到)、500(内部服务器错误)等状态码。可以在
dio
请求的options
中设置validateStatus
回调来自定义对服务器响应状态码的处理。如果状态码不符合预期,可以抛出异常或进行其他处理。 - 文件读写错误:在文件下载保存或上传读取文件时可能发生。例如目标目录不存在、文件权限不足等。在进行文件操作前,应该先检查目录是否存在,并且确保应用具有相应的文件读写权限。
- 网络连接失败:这通常是由于网络不可用或服务器无法访问导致的。在Dio中,这类错误通常会抛出
- 优化措施
- 缓存:对于频繁下载的文件,可以考虑使用缓存机制。在下载文件前,先检查本地缓存中是否存在该文件,如果存在且未过期,则直接使用缓存文件,避免重复下载。
- 优化网络请求:尽量合并多个小的网络请求为一个大请求,减少网络开销。同时,合理设置请求超时时间,避免长时间等待无响应的请求。
- 资源管理:在文件上传和下载完成后,及时释放相关资源,例如关闭文件流、取消未完成的请求等,防止内存泄漏。
通过合理处理错误和采取优化措施,可以提高基于Dio的文件上传与下载功能的稳定性和性能,为用户提供更好的体验。