file_saver_ffi 0.1.0
file_saver_ffi: ^0.1.0 copied to clipboard
A high-performance file saver for Flutter using FFI and JNI. Effortlessly save to gallery (images/videos) or device storage with original quality and custom album support.
File Saver FFI #
A high-performance file saver for Flutter using FFI and JNI. Effortlessly save to gallery (images/videos) or device storage with original quality and custom album support.
Features #
- 🖼️ Gallery Saving – Save images and videos to iOS Photos or Android Gallery with custom albums
- ⚡ Native Performance – Powered by FFI (iOS) and JNI (Android) for near-zero latency
- 📁 Universal Storage – Save any file type (PDF, ZIP, DOCX, etc.) to device storage
- 💾 Original Quality – Files saved bit-for-bit without compression or metadata loss
- 📊 Progress & Cancellation – Real-time progress tracking with cancellable operations
- ⚙️ Conflict Resolution – Auto-rename, overwrite, skip, or fail on existing files
If you want to say thank you, star us on GitHub or like us on pub.dev.
🤖 Ask AI About This Library #
Have questions about file_saver_ffi? Get instant AI-powered answers about the library's features, usage, and best practices.
→ Chat with AI Documentation Assistant
Ask anything like:
- "How do I save a video to the gallery with progress tracking?"
- "What's the difference between saveBytes and saveFile?"
- "How to handle permission errors on Android 10+?"
- "Show me examples of custom file types"
Installation #
First, follow the package installation instructions and add file_saver_ffi to your app.
Quick Start #
Platform Setup #
Android Configuration
Add to android/app/src/main/AndroidManifest.xml:
<!-- Only required for Android 9 (API 28) and below -->
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
Note: Android 10+ uses scoped storage automatically and does not require this permission.
Supported: API 21+ (Android 5.0+)
iOS Configuration
Add to ios/Runner/Info.plist:
<!-- For Photos Library Access (images/videos) -->
<key>NSPhotoLibraryAddUsageDescription</key>
<string>This app needs permission to save photos and videos to your library</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>This app needs permission to access your photo library</string>
<!-- Prevent automatic "Select More Photos" prompt on iOS 14+ -->
<key>PHPhotoLibraryPreventAutomaticLimitedAccessAlert</key>
<true/>
<!-- Optional: Make files visible in Files app -->
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
Supported: iOS 13.0+
Basic Usage #
import 'package:file_saver_ffi/file_saver_ffi.dart';
try {
// Save image bytes
final uri = await FileSaver.instance.saveBytesAsync(
fileBytes: imageBytes,
fileName: 'my_image',
fileType: ImageType.jpg,
);
print('Saved to: $uri');
} on PermissionDeniedException catch (e) {
print('Permission denied: ${e.message}');
} on FileSaverException catch (e) {
print('Save failed: ${e.message}');
}
Core Concepts #
API Methods #
The library provides methods organized by input source and API style:
Input Sources
- Bytes (
save*Bytes*) - Save data from memory (Uint8List) - File (
save*File*) - Save from file path (efficient for large files) - Network - Download and save from URL (planned)
API Styles
- Stream (
save*) - Full control with progress events and cancellation - Async (
save*Async) - Simple Future-based API with optional progress callback - Interactive (
save*As) - User picks save location (planned)
API Matrix
| Input Source | Stream API | Async API | Interactive |
|---|---|---|---|
| Bytes | saveBytes() |
saveBytesAsync() |
saveBytesAs() |
| File | saveFile() |
saveFileAsync() |
saveFileAs() |
| Network | saveNetworkFile() |
saveNetworkFileAsync() |
saveNetworkFileAs() |
When to use:
save*()- Need real-time progress, cancellation, or full event controlsave*Async()- Simple save with optional progress callbacksave*As()- Let user choose save location via system picker
File Types #
The library supports 35+ file formats across 4 categories:
| Category | Formats | Count | Example Types |
|---|---|---|---|
| Images | PNG, JPG, GIF, WebP, BMP, HEIC, HEIF, TIFF, ICO, DNG | 12 | ImageType.png, ImageType.jpg |
| Videos | MP4, MOV, MKV, WebM, AVI, 3GP, M4V, FLV, WMV, HEVC | 12 | VideoType.mp4, VideoType.mov |
| Audio | MP3, AAC, WAV, FLAC, OGG, M4A, AMR, Opus, AIFF, CAF | 11 | AudioType.mp3, AudioType.aac |
| Custom | Any format via extension + MIME type | ∞ | CustomFileType(ext: 'pdf', mimeType: 'application/pdf') |
Save Locations #
Control where files are saved with platform-specific options:
| Location | Android | iOS | Use Case |
|---|---|---|---|
| Default | Downloads/ | Documents/ | General files (no location specified) |
pictures |
Pictures/ | Photos Library* | Images |
movies |
Movies/ | Photos Library* | Videos |
music |
Music/ | Documents/ | Audio files |
downloads |
Downloads/ | Documents/ | Downloads |
dcim |
DCIM/ | Photos Library* | Camera photos |
photos |
N/A | Photos Library | iOS Photos (requires permission) |
documents |
N/A | Documents/ | iOS Documents (no permission) |
* When using IosSaveLocation.photos, otherwise saves to Documents/
Example:
import 'dart:io' show Platform;
final uri = await FileSaver.instance.saveBytesAsync(
fileBytes: imageBytes,
fileName: 'photo',
fileType: ImageType.jpg,
saveLocation: Platform.isAndroid
? AndroidSaveLocation.pictures
: IosSaveLocation.photos,
);
Conflict Resolution #
Handle existing files with 4 strategies:
| Strategy | Behavior | Use Case |
|---|---|---|
autoRename (default) |
Appends (1), (2), etc. to filename | Safe, prevents data loss |
overwrite |
Replaces existing file* | Update existing files |
fail |
Throws FileExistsException |
Strict validation |
skip |
Returns existing file URI | Idempotent saves |
* Platform limitations:
- iOS Photos: Can only overwrite files owned by your app
- Android 10+: Can only overwrite files owned by your app (scoped storage)
Common Use Cases #
Save to Gallery #
import 'dart:io' show Platform;
// Save image to Photos Library (iOS) or Pictures (Android)
final uri = await FileSaver.instance.saveBytesAsync(
fileBytes: imageBytes,
fileName: 'photo',
fileType: ImageType.jpg,
saveLocation: Platform.isAndroid
? AndroidSaveLocation.pictures
: IosSaveLocation.photos,
subDir: 'My App', // Creates album (iOS) or folder (Android)
);
Progress Tracking #
// Stream API - Full control
await for (final event in FileSaver.instance.saveBytes(
fileBytes: largeVideoBytes,
fileName: 'video',
fileType: VideoType.mp4,
)) {
switch (event) {
case SaveProgressStarted():
showLoadingIndicator();
case SaveProgressUpdate(:final progress):
updateProgressBar(progress); // 0.0 to 1.0
case SaveProgressComplete(:final uri):
hideLoadingIndicator();
showSuccess('Saved to: $uri');
case SaveProgressError(:final exception):
hideLoadingIndicator();
showError(exception.message);
case SaveProgressCancelled():
showCancelled();
}
}
// Async API - Simple callback
final uri = await FileSaver.instance.saveBytesAsync(
fileBytes: largeVideoBytes,
fileName: 'video',
fileType: VideoType.mp4,
onProgress: (progress) {
print('Progress: ${(progress * 100).toInt()}%');
},
);
Note: Progress is reported in 1MB chunks. For iOS Photos Library saves, progress jumps from 0% to 100% due to API limitations.
Cancellation #
StreamSubscription<SaveProgress>? subscription;
subscription = FileSaver.instance.saveBytes(
fileBytes: largeVideoBytes,
fileName: 'video',
fileType: VideoType.mp4,
).listen((event) {
switch (event) {
case SaveProgressUpdate(:final progress):
updateProgressBar(progress);
case SaveProgressCancelled():
showMessage('Cancelled and cleaned up!');
// ... handle other events
}
});
// Cancel when needed
cancelButton.onPressed = () {
subscription?.cancel(); // Stops I/O, deletes partial file, emits SaveProgressCancelled
};
Error Handling #
try {
final uri = await FileSaver.instance.saveBytesAsync(
fileBytes: pdfBytes,
fileName: 'document',
fileType: CustomFileType(ext: 'pdf', mimeType: 'application/pdf'),
);
print('✅ Saved: $uri');
} on PermissionDeniedException catch (e) {
print('❌ Permission denied: ${e.message}');
// Request permissions
} on FileExistsException catch (e) {
print('❌ File exists: ${e.fileName}');
// Handle conflict
} on StorageFullException catch (e) {
print('❌ Storage full: ${e.message}');
// Show storage full message
} on FileSaverException catch (e) {
print('❌ Save failed: ${e.message}');
// Generic error handling
}
Platform Differences #
Storage Locations #
| Aspect | Android | iOS |
|---|---|---|
| Default location | Downloads/ | Documents/ |
| Gallery access | MediaStore (no permission on 10+) | Photos Library (requires permission) |
| Custom files | Public directories via MediaStore | App sandbox (Documents/) |
| File visibility | Visible in file managers | Visible in Files app if UIFileSharingEnabled |
Overwrite Behavior #
| Scenario | Android 9- | Android 10+ | iOS Photos | iOS Documents |
|---|---|---|---|---|
| Own files | ✅ Overwrite | ✅ Overwrite | ✅ Overwrite | ✅ Overwrite |
| Other apps' files | ✅ Overwrite | ⚠️ Auto-rename* | ⚠️ Duplicate | N/A (sandboxed) |
* Android 10+ scoped storage cannot detect files from other apps before saving
SubDir Parameter #
- Android: Creates folder in MediaStore collection (e.g.,
Pictures/My App/) - iOS Photos: Creates album with specified name
- iOS Documents: Creates subdirectory (e.g.,
Documents/My App/)
Exception Reference #
| Exception | Description | Error Code |
|---|---|---|
PermissionDeniedException |
Storage access denied | PERMISSION_DENIED |
FileExistsException |
File exists with fail strategy |
FILE_EXISTS |
StorageFullException |
Insufficient device storage | STORAGE_FULL |
InvalidFileException |
Empty bytes or invalid filename | INVALID_FILE |
FileIOException |
File system error | FILE_IO_ERROR |
UnsupportedFormatException |
Format not supported on platform | UNSUPPORTED_FORMAT |
SourceFileNotFoundException |
Source file not found (saveFile) | FILE_NOT_FOUND |
ICloudDownloadException |
iCloud download failed (iOS) | ICLOUD_DOWNLOAD_FAILED |
CancelledException |
Operation cancelled by user | CANCELLED |
PlatformException |
Generic platform error | PLATFORM_ERROR |
API Reference #
Bytes Methods #
Save data from memory (Uint8List).
| Method | Returns | Description |
|---|---|---|
saveBytes() |
Stream<SaveProgress> |
Stream API with full control, cancellation, and progress events |
saveBytesAsync() |
Future<Uri> |
Async API with optional progress callback |
Common Parameters:
fileBytes(required) - File content asUint8ListfileName(required) - File name without extensionfileType(required) -ImageType,VideoType,AudioType, orCustomFileTypesaveLocation(optional) - Platform-specific save location (defaults: Android=Downloads, iOS=Documents)subDir(optional) - Subdirectory/album nameconflictResolution(optional) - Default:ConflictResolution.autoRenameonProgress(optional, Async only) - Progress callback(double progress) => void
File Methods #
Save from file path (efficient for large files, no memory loading).
| Method | Returns | Description |
|---|---|---|
saveFile() |
Stream<SaveProgress> |
Stream API with full control, cancellation, and progress events |
saveFileAsync() |
Future<Uri> |
Async API with optional progress callback |
Common Parameters:
filePath(required) - Source file path (file://orcontent://URI)fileName(required) - Target file name without extensionfileType(required) -ImageType,VideoType,AudioType, orCustomFileTypesaveLocation(optional) - Platform-specific save locationsubDir(optional) - Subdirectory/album nameconflictResolution(optional) - Default:ConflictResolution.autoRenameonProgress(optional, Async only) - Progress callback(double progress) => void
iOS iCloud Support: When saving files from iCloud Drive, progress shows download (0-50%) + save (50-100%).
Network Methods #
Planned for future release - download and save files from URLs.
SaveProgress Events #
Stream API emits these sealed class events:
| Event | Properties | Description |
|---|---|---|
SaveProgressStarted |
- | Operation began |
SaveProgressUpdate |
progress: double |
Progress from 0.0 to 1.0 |
SaveProgressComplete |
uri: Uri |
Success with saved file URI |
SaveProgressError |
exception: FileSaverException |
Error occurred |
SaveProgressCancelled |
- | User cancelled operation |
Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.
Future Features #
File Input Methods- Save from Network URL
- User-Selected Location Android (SAF), iOS (Document Picker)
- Custom Path Support
Progress TrackingCancellation SupportSave from File Path- MacOS Support
- Windows Support
- Web Support
License #
MIT License - see LICENSE file for details.