Flutter笔记关于Flutter中的大文件上传(上)大文件上传背景与 Flutter 端实现文件分片传输
1. 概述
1.1 大文件上传的应用场景
在Flutter应用中,大文件上传的场景非常常见。例如,用户可能需要上传高清照片、视频或其他大型媒体文件。在企业应用中,用户可能需要上传大量的数据文件或报告。本文的主题是在Flutter应用中实现大文件上传。我们将探讨大文件上传的常见问题,介绍一些有效的上传策略,并提供实现这些策略的Flutter代码示例。我们的目标是帮助读者理解如何在Flutter应用中有效地上传大文件。
1.2 大文件上传的挑战
大文件上传面临许多挑战。首先,网络不稳定可能导致上传失败或速度慢。其次,由于文件大小,上传可能需要很长时间,而上传进度可能不明确,导致用户体验不佳。此外,如果应用在上传过程中崩溃或被关闭,可能需要从头开始上传,这会浪费大量时间和带宽。
1.3 整体上传局限性与优化策略
传统的文件上传方法通常是将整个文件作为一个请求体发送到服务器。然而,这种方法对大文件来说并不理想。由于网络不稳定,大文件有更大的可能性在上传过程中失败。此外,如果应用在上传过程中崩溃或被关闭,可能需要从头开始上传。
为了解决这些问题,我们可以采用一些策略来优化大文件的上传。
1.3.1 分块上传
例如,我们可以使用分块上传,将文件分成多个小块,然后分别上传。这样,即使某个块上传失败,也只需要重新上传该块,而不是整个文件。
1.3.2 断点续传
另一种策略是断点续传,它允许我们在上传被中断后从中断点继续上传,而不是从头开始。这两种策略各有优缺点。分块上传可以提高上传的成功率,但可能需要更复杂的服务器支持。断点续传可以节省带宽,但需要服务器支持范围请求,并且需要在客户端保存上传的进度。
2. 在Flutter中实现大文件上传前的准备
在这一章节中,我们将详细介绍如何在Flutter中实现大文件上传。我们将通过以下步骤来实现这个过程:获取权限、选择文件、分块、上传和处理错误。
2.1 获取权限
在用户选择文件之前,我们需要确保应用有读取文件的权限。我们可以使用permission_handler插件来请求权限。首先,需要在pubspec.yaml文件中添加permission_handler依赖以获取权限。
dependencies: permission_handler: 替换你安装的版本号
或者直接在项目中运行下面的命令安装最新版:
flutter pub add permission_handler
然后,我们可以使用Permission类的request方法来请求权限:
import 'package:permission_handler/permission_handler.dart'; void requestPermission() async { var status = await Permission.storage.status; if (!status.isGranted) { status = await Permission.storage.request(); } }
2.2 文件选择
在获取了权限后,我们可以使用file_picker插件来选择文件。首先,需要在pubspec.yaml文件中添加依赖:
dependencies: file_picker: 替换你安装的版本号
或者直接在项目中运行下面的命令安装最新版:
flutter pub add file_picker
然后,可以使用FilePicker的getFiles方法来选择文件:
import 'package:file_picker/file_picker.dart'; void pickFile() async { FilePickerResult? result = await FilePicker.platform.pickFiles(); if(result != null) { PlatformFile file = result.files.first; print(file.name); print(file.bytes); print(file.size); print(file.extension); print(file.path); } else { // User canceled the picker } }
3. 文件的切片
3.1 基本思路
在选择了文件后,我们需要将文件分块。分块的目的是将大文件切分成多个小块,这样我们可以逐个上传这些小块,而不是一次性上传整个大文件。这种方法有两个优点:
- 如果某个块上传失败,我们只需要重新上传该块,而不是整个文件;
- 我们可以更精确地追踪上传进度,因为我们可以知道已经上传了多少块和还剩下多少块。
3.2 实现步骤
在Flutter中,我们可以通过以下步骤来实现文件分块:
- 首先,我们需要 获取文件的字节数据。
我们可以使用 PlatformFile 对象的 bytes 属性来获取这些数据。这将返回一个 Uint8List 对象,它是一个包含文件所有字节的列表。 - 然后,我们需要 创建一个新的列表来存储分块的数据。
我们可以创建一个空的 List<int> 对象来存储这些数据。 - 接下来,我们需要 遍历文件的字节数据,并将数据分块。
我们可以使用一个循环来遍历字节数据。在每次迭代中,我们都会取出一块数据,并将这块数据添加到分块数据的列表中。 - 最后,返回分块数据的列表。
3.3 示范案例
一个具体的实现代码如下:
List<int> splitFile(PlatformFile file, int chunkSize) { List<int> chunks = []; List<int> bytes = file.bytes!.toList(); for (int i = 0; i < bytes.length; i += chunkSize) { int end = (i + chunkSize < bytes.length) ? i + chunkSize : bytes.length; chunks.add(bytes.sublist(i, end)); } return chunks; }
其中,chunkSize 参数表示每个块的大小。
bytes.sublist(i, end) 方法用于获取从 i 到 end 的子列表,这就是我们的一个块。
然后,将这个块添加到 chunks 列表中。这个过程会一直重复,直到所有的字节数据都被分块。
4. 切片的上传
在分块后,我们可以逐个上传这些块。在这个过程中,我们需要创建一个 HTTP 请求,将每个块作为请求的一部分,然后发送这个请求到服务器。我们可以使用 http库 来创建和发送 HTTP 请求。
以下是实现这个功能的步骤:
- 首先,我们需要创建一个 MultipartRequest 对象。
这个对象表示一个 multipart/form-data 请求,它可以包含多个文件和其他类型的数据。我们需要指定请求的方法(在这个例子中是 POST )和 URL。 - 然后,我们需要将块添加到请求中。
我们可以使用 MultipartFile 类的 fromBytes 方法来创建一个文件对象,然后将这个对象添加到请求的 files 列表中。 - 接下来,我们需要发送请求。
我们可以使用 send 方法来发送请求,这个方法会返回一个 StreamedResponse 对象。 - 最后,我们需要检查响应的状态码,以确定上传是否成功。
如果状态码是 200,那么上传成功;否则,上传失败。
例如:
import 'package:http/http.dart' as http; void uploadChunk(List<int> chunk, String url) async { var request = http.MultipartRequest('POST', Uri.parse(url)); request.files.add(http.MultipartFile.fromBytes('file', chunk)); var response = await request.send(); if (response.statusCode == 200) { print('Uploaded!'); } else { print('Failed to upload.'); } }
其中:
chunk 参数是我们要上传的块,url 参数是服务器的 URL;
MultipartFile.fromBytes 方法的第一个参数是文件的名字,第二个参数是文件的字节数据;
send 方法会异步发送请求,所以我们需要使用 await 关键字来等待它完成。
5. 完整实现
作为Flutter端完整实现我简单写了一下,上传到了 Git Hub 上,并发布在 Pub.dev,地址为:https://pub.dev/packages/slivers_uploader。本文中各个交接讲解的仅仅是思路,而在该实现的版本中,做了诸多必要的调整。我将文件选择和上传分开。
一个Flutter端示例如下:
import 'package:flutter/material.dart'; import 'package:slivers_uploader/slivers_uploader.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'File Uploader Example', theme: ThemeData( primarySwatch: Colors.blue, ), home: MyHomePage(), ); } } class MyHomePage extends StatelessWidget { final uploader = FileUploader( chunkSize: 50 * 1024 * 1024, // 指定分块大小,默认 1024*1024 ); MyHomePage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('File Uploader Example'), ), body: Center( child: TextButton( child: const Text('Upload File'), onPressed: () async { var file = await uploader.pickFile(); await file.upload( url: 'http://192.168.31.239:8001/upload/', onSuccess: (chunkNumber) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Chunk $chunkNumber upload successful!')), ); }, onError: (chunkNumber) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Chunk $chunkNumber upload failed.')), ); }, ); }, ), ), ); } }
大概效果看起来是这样的:
在后端对应的可以看到通过POST发送过来的提交记录,就像我的后端这样:
下一篇文章,我们主要介绍 Django 后端如何实现对应的分块上传的文件。