file_saver_ffi 0.0.3
file_saver_ffi: ^0.0.3 copied to clipboard
A Flutter plugin for saving files to device storage using FFI and JNI
File Saver FFI #
A high-performance Flutter plugin for saving files, images, and videos to device storage using native APIs via FFI (iOS) and JNI (Android).
Features #
- ⚡ Native Performance - FFI (iOS) and JNI (Android) for maximum speed
- 📁 Universal File Saving - Save any file type to device storage with a single method
- 🖼️ Image-Specific Handling - Format validation and album support
- 🎥 Video Handling - Native integration with Photos (iOS) and MediaStore (Android)
- ⚙️ Conflict Resolution - Auto-rename, overwrite, skip, or fail on conflicts
- 🎯 Album/Subdirectory Support - Organize all file types in albums (iOS) or subdirectories (Android)
- 💾 Original Quality - Always saves at original quality, no compression
- 🔒 Type-Safe API - Sealed classes and pattern matching for robust code
- 📂 Smart Location Routing - Files automatically saved to appropriate directories based on type
If you want to say thank you, star us on GitHub or like us on pub.dev.
Supported Platforms #
| Platform | Minimum Version | Notes |
|---|---|---|
| Android | API 21+ (Android 5.0+) | Scoped storage for Android 10+ |
| iOS | 13.0+ | Photos framework with album support |
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+ (API 29+) uses scoped storage automatically and does not require this permission.
iOS Configuration #
Add to ios/Runner/Info.plist:
For Photos Library Access (Required for 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/>
Note: On iOS 14+, if the user selects "Limited Photos" access, iOS may automatically show a dialog prompting them to select more photos. The
PHPhotoLibraryPreventAutomaticLimitedAccessAlertkey prevents this automatic dialog, providing a better user experience.
For Files App Visibility (Optional for custom files)
Files are saved to the Application Documents Directory. To make them visible to users in the Files app, add:
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
Quick Start #
import 'package:file_saver_ffi/file_saver_ffi.dart';
try {
// Save image bytes
final uri = await FileSaver.instance.saveBytes(
bytes: 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}');
}
Resource Management #
FileSaver uses native resources via FFI (iOS) and JNI (Android). The library provides automatic cleanup via NativeFinalizer, but you can also manually release resources if needed.
Manual Disposal #
If you want to release native resources immediately (e.g., to free memory sooner), call dispose():
// Release resources immediately when you're done
FileSaver.instance.dispose();
App Lifecycle Integration (Optional) #
For explicit cleanup when the app terminates, you can use WidgetsBindingObserver:
import 'package:file_saver_ffi/file_saver_ffi.dart';
import 'package:flutter/material.dart';
class AppLifecycleObserver extends WidgetsBindingObserver {
final VoidCallback? onDetached;
AppLifecycleObserver({this.onDetached});
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.detached) {
onDetached?.call();
}
}
}
void main() {
final binding = WidgetsFlutterBinding.ensureInitialized();
binding.addObserver(
AppLifecycleObserver(
onDetached: FileSaver.instance.dispose,
),
);
runApp(const MyApp());
}
Note:
AppLifecycleState.detachedis not guaranteed to be called on all platforms when the app is force-killed. However, the OS will automatically reclaim all memory when the process terminates, so this is primarily for explicit cleanup in normal shutdown scenarios.
## Supported File Types
### Images (12 formats)
`PNG`, `JPG`, `JPEG`, `GIF`, `WebP`, `BMP`, `HEIC`, `HEIF`, `TIFF`, `TIF`, `ICO`, `DNG`
```dart
ImageType.png
ImageType.jpg
ImageType.gif
ImageType.webp
// ... and more
Videos (12 formats) #
MP4, 3GP, WebM, M4V, MKV, MOV, AVI, FLV, WMV, HEVC, VP9, AV1
VideoType.mp4
VideoType.mov
VideoType.mkv
// ... and more
Audio (11 formats) #
MP3, AAC, WAV, AMR, 3GP, M4A, OGG, FLAC, Opus, AIFF, CAF
AudioType.mp3
AudioType.aac
AudioType.wav
// ... and more
Custom File Types #
Support any file format by specifying extension and MIME type:
CustomFileType(
ext: 'pdf',
mimeType: 'application/pdf'
)
CustomFileType(
ext: 'docx',
mimeType:'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
)
Conflict Resolution Strategies #
Control what happens when a file with the same name already exists:
| Strategy | Behavior | Use Case |
|---|---|---|
autoRename (default) |
Appends (1), (2), etc. to filename | Safe, prevents data loss |
overwrite |
Replaces existing file | Update existing files |
fail |
Returns SaveFailure with "FILE_EXISTS" error |
Strict validation |
skip |
Returns SaveSuccess with existing file path |
Idempotent saves |
Overwrite Behavior (Platform-Specific) #
The overwrite strategy behaves differently across platforms:
iOS
Images & Videos (Photos Library):
- ✅ Files owned by your app - Successfully overwritten (deletes old, adds new)
- ⚠️ Files from other apps - Cannot be deleted; iOS allows duplicate names to coexist
- iOS Photos Library has built-in mechanisms to keep files with the same name from different apps
- Your file will be added alongside the existing file (both will exist)
Custom Files (Documents Directory):
- ✅ Full overwrite capability - Each app has its own sandbox
- Files from other apps are isolated and inaccessible due to iOS sandbox security
Android 9 and Below (API 28-)
- ✅ Full overwrite capability - Can replace any existing file
- Requires
WRITE_EXTERNAL_STORAGEpermission
Android 10+ (API 29+)
All File Types (MediaStore):
- ✅ Files owned by your app - Successfully overwritten
- ⚠️ Files from other apps - Cannot be detected; will be auto-renamed instead
Important Platform Limitation:
Due to Scoped Storage security, files created by other apps cannot be detected before saving. The library can only detect and handle conflicts for files owned by your app.
What happens when a file from another app exists:
- With
autoRename: MediaStore automatically renames your file (e.g.,photo.jpg→photo (1).jpg) - With
overwrite: Your file will be auto-renamed instead of overwriting (same asautoRename) - With
failorskip: Behavior is unpredictable as the conflict cannot be detected
Why this happens: Android's Scoped Storage uses different APIs with different scopes:
- Query API (used for conflict detection): Scoped to your app's files only
- Insert API (used for saving): Has global check to prevent overwrites
This is Android's platform design for security, not a library limitation.
Platform Comparison Summary #
| Scenario | iOS Photos | iOS Documents | Android 9- | Android 10+ |
|---|---|---|---|---|
| Own files | ✅ Overwrite | ✅ Overwrite | ✅ Overwrite | ✅ Overwrite |
| Other apps' files | ⚠️ Duplicate | N/A (sandboxed) | ✅ Overwrite | ⚠️ Auto-rename |
Example #
try {
final uri = await FileSaver.instance.saveBytes(
bytes: fileBytes,
fileName: 'document',
fileType: CustomFileType(ext: 'pdf', mimeType: 'application/pdf'),
conflictResolution: ConflictResolution.autoRename,
);
// If "document.pdf" exists, saves as "document (1).pdf"
print('Saved to: $uri');
} on FileSaverException catch (e) {
print('Error: ${e.message}');
}
Advanced Usage #
Save with Subdirectory/Album #
try {
final uri = await FileSaver.instance.saveBytes(
bytes: videoBytes,
fileName: 'vacation_video',
fileType: VideoType.mp4,
subDir: 'My Vacations', // Creates album on iOS, folder on Android
);
print('Video saved to: $uri');
} on FileSaverException catch (e) {
print('Error: ${e.message}');
}
Complete Example with Error Handling #
try {
final uri = await FileSaver.instance.saveBytes(
bytes: pdfBytes,
fileName: 'invoice_${DateTime.now().millisecondsSinceEpoch}',
fileType: CustomFileType(ext: 'pdf', mimeType: 'application/pdf'),
subDir: 'Invoices',
conflictResolution: ConflictResolution.autoRename,
);
print('✅ Saved successfully!');
print('URI: $uri');
} on PermissionDeniedException catch (e) {
print('❌ Permission denied: ${e.message}');
// Request permissions
} on FileExistsException catch (e) {
print('❌ File already exists: ${e.fileName}');
// Handle conflict
} on StorageFullException catch (e) {
print('❌ Storage full: ${e.message}');
// Show storage full message
} on InvalidFileException catch (e) {
print('❌ Invalid file: ${e.message}');
// Validate file data
} on FileSaverException catch (e) {
print('❌ Save failed: ${e.message}');
// Generic error handling
}
Platform-Specific Behavior #
File Storage Locations #
Android
Files are saved to MediaStore collections based on type:
| File Type | Location |
|---|---|
| Images | Pictures/[subDir]/ |
| Videos | Movies/[subDir]/ |
| Audio | Music/[subDir]/ |
| Custom Files | Downloads/[subDir]/ |
URI Format: content://media/external/...
iOS
Files are saved to platform-appropriate locations:
| File Type | Location |
|---|---|
| Images | Photos library album [subDir] |
| Videos | Photos library album [subDir] |
| Audio | Photos library (if supported) |
| Custom Files | Documents/[subDir]/ (visible in Files app if configured) |
URI Format: ph:// for Photos, file:// for Documents
SubDir Parameter #
- iOS: Creates an album in the Photos app with the specified name
- Android: Creates a folder in the appropriate MediaStore collection
Example:
// iOS: Creates "My App" album in Photos
// Android: Creates Pictures/My App/ folder
subDir: 'My App'
Error Handling #
The library provides specific exception types for different failure scenarios:
| 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 |
UnsupportedFormatException |
Format not supported on platform | UNSUPPORTED_FORMAT |
PlatformException |
Generic platform-specific error | PLATFORM_ERROR |
Handling Errors #
try {
final uri = await FileSaver.instance.saveBytes(...);
print('Saved to: $uri');
} on PermissionDeniedException catch (e) {
// Request permissions
print('Permission denied: ${e.message}');
} on FileExistsException catch (e) {
// File already exists with fail strategy
print('File already exists: ${e.fileName}');
} on StorageFullException catch (e) {
// Show storage full message
print('Storage full: ${e.message}');
} on FileSaverException catch (e) {
// Generic error handling
print('Save failed: ${e.message}');
}
API Reference #
FileSaver #
Singleton API class for saving files.
Future<Uri> saveBytes({
required Uint8List bytes,
required String fileName,
required FileType fileType,
String? subDir,
ConflictResolution conflictResolution = ConflictResolution.autoRename,
})
Throws FileSaverException or subtypes on failure.
ConflictResolution #
Enum for conflict resolution strategies:
enum ConflictResolution {
autoRename, // Append (1), (2), etc.
overwrite, // Replace existing file
fail, // Return error
skip, // Return existing file path
}
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 Tracking
- MacOS Support
- Windows Support
- Web Support