欢迎关注微信公众号:FSA全栈行动 👋
一、为什么这件事没那么简单
在做 Flutter 开发时,下载一个图片或者视频其实并不难。最让人头疼的往往是:下载完之后,怎么让这些文件乖乖出现在用户的手机相册(Gallery)里。
乍一看这事儿挺简单:下载文件,存到本地,完事。
但实际操作起来,Android 和 iOS 的存储逻辑完全不同。Android 的权限控制随着版本升级变了又变;有时候你还希望把文件分门别类存进特定的相册,或者想避开重复保存。
以前遇到这种需求,我可能得针对 Android 和 iOS 分别写原生代码去处理,折腾起来非常麻烦。最近我在找更轻量化的方案时,发现了 saver_gallery 这个库。它能直接从 Flutter 端实现保存图片、视频到相册,还支持自定义相册路径和批量保存,非常省心。
二、上手实战
1、基础配置
首先,在 pubspec.yaml 中引入 saver_gallery:
YAML
dependencies:
saver_gallery: ^5.1.0
然后在终端跑一下 flutter pub get 即可。
2、保存图片到相册
最常见的场景就是从 API 获取图片字节流(bytes),然后直接存入相册。这里我结合 Dio 来演示一下:
Dart
import 'dart:typed_data';
import 'package:dio/dio.dart';
import 'package:saver_gallery/saver_gallery.dart';
Future<void> saveImage() async {
// 1. 下载图片字节流
final response = await Dio().get(
'https://example.com/image.jpg',
options: Options(responseType: ResponseType.bytes),
);
// 2. 写入相册
final result = await SaverGallery.saveImage(
Uint8List.fromList(response.data),
quality: 100,
fileName: 'my_photo.jpg',
albumPath: 'MyApp/Images', // 自定义相册名
skipIfExists: false, // 是否跳过已存在的文件
);
print('保存结果: $result');
}
这里 albumPath 特别有用,它可以帮你把图片归类到指定的文件夹里,而不是全都堆在相机默认目录下。
3、保存视频到相册
视频的逻辑跟图片略有不同。因为视频文件通常比较大,我们通常先把它下载到设备的临时目录(Temporary Directory),然后再进行移动或保存。
Dart
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';
import 'package:saver_gallery/saver_gallery.dart';
Future<void> saveVideo() async {
// 1. 获取临时目录
final tempDir = await getTemporaryDirectory();
final videoPath = '${tempDir.path}/sample_video.mp4';
// 2. 先下载到临时文件
await Dio().download('https://example.com/video.mp4', videoPath);
// 3. 移动到相册
final result = await SaverGallery.saveFile(
filePath: videoPath,
fileName: 'my_video.mp4',
albumPath: 'MyApp/Videos',
skipIfExists: false,
);
print('视频保存结果: $result');
}
4、进阶:自定义相册与批量保存
如果你需要一次性下载一批图片(比如从一组壁纸里选),用 saveImages 这种批量接口效率更高。
| 需求场景 | 使用方法 | 核心逻辑 |
|---|---|---|
| 单张图片 | saveImage |
直接传入 Uint8List |
| 单张视频 | saveFile |
需要先下载到本地路径再调用 |
| 多张图片 | saveImages |
构造 List |
| 多段视频 | saveFiles |
构造 List |
批量保存的示例代码如下:
Dart
// 批量保存图片
final images = [
SaveImageData(bytes: bytes1, fileName: '1.jpg', albumPath: 'MyAlbum'),
SaveImageData(bytes: bytes2, fileName: '2.jpg', albumPath: 'MyAlbum'),
];
await SaverGallery.saveImages(images);
三、避坑指南:权限与细节
搞定功能后,最容易在"权限"上栽跟头。
1、权限处理
存储权限在不同版本的 Android 上表现非常复杂。在旧版本上,你可能需要申请 WRITE_EXTERNAL_STORAGE;而在 Android 10 及更高版本,由于"分区存储(Scoped Storage)"的引入,逻辑变了很多。
针对这种多版本差异,我建议配合 permission_handler 库来统一管理权限。
对于 iOS 端,你需要在 Info.plist 中添加以下配置,否则 App 在尝试写入相册时会直接闪退:
XML
<key>NSPhotoLibraryAddUsageDescription</key>
<string>我们需要访问您的相册以便保存图片</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>我们需要访问您的相册以便保存图片</string>
2、如何避免重复下载
这里有一个很实用的参数:skipIfExists: true。
如果你的 App 支持重复点击下载,或者用户可能多次触发同一张图的保存,开启这个开关可以有效防止手机相册里出现一堆重复的、一模一样的图片。
3、如何获取保存后的位置
保存成功后,SaverGallery 会返回一个 result 对象。
注意,这个返回的 savedUri 在不同平台上表现不同:
-
新版
Android可能返回content://类型的 URI。 -
旧版可能返回
file://类型的路径。 -
iOS则返回其专属的媒体资源 ID。
所以,在处理返回结果时,把它当成一个"平台媒体标识符"即可,不要强行把它当成普通文件系统路径去处理。
四、总结
保存媒体文件看似是个小功能,但涉及到权限、分区存储、自定义相册以及多平台兼容性时,细节确实不少。
saver_gallery 给我的感觉是:它把复杂的原生调用封装成了一套非常直观的 API。如果你不想在 Android 的 Scoped Storage 和 iOS 的 Info.plist 规则里"摸黑瞎改",这个库确实是个不错的选择。
如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~