Skip to main content

file_saver_ffi 0.12.0 copy "file_saver_ffi: ^0.12.0" to clipboard
file_saver_ffi: ^0.12.0 copied to clipboard

Save bytes, files and downloads to the gallery, device storage or a user-picked folder, with progress, cancellation and conflict handling. Native FFI and JNI on mobile.

cover

File Saver FFI #

Platform License: MIT Ask DeepWiki

A high-performance file saver for Flutter supporting Android, iOS, macOS, Windows, Linux, and Web. Save to gallery, device storage, or a user-chosen directory — with real-time progress tracking.

Features #

  • đŸ–ŧī¸ Gallery Saving – Save images and videos to iOS Photos or Android Gallery with custom albums
  • ⚡ Native Performance – Swift through FFI on iOS/macOS, Kotlin through JNI on Android, and dart:io on Windows/Linux, with no method channel in between
  • 🌐 Web Support – Browser download via anchor element or File System Access API (FSA) for Chrome/Edge
  • 📁 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
  • 📂 Remembered Directories – Keep access to a user-picked directory across app restarts (Android, iOS, macOS, Windows, Linux)

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 save and saveAs?"
  • "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

Supported: API 24+ (Android 7.0+). Apps on Android Gradle Plugin 8 or 9 build it, with AGP 9's built-in Kotlin on or off. With built-in Kotlin off (and on AGP 8), Flutter compiles the plugin's Kotlin with the Kotlin Gradle Plugin the app declares in android/settings.gradle(.kts), as flutter create sets it up.

For Standard Save Locations (Downloads, Pictures, etc.), no configuration is needed. The plugin automatically:

  • Declares WRITE_EXTERNAL_STORAGE permission (merged via manifest merger, only applies to API ≤ 28)
  • Requests runtime permission when needed (Android 9 and below)
  • Uses scoped storage on Android 10+ (no permission required)

Custom Paths (PathLocation) #

Saving to custom paths on Android 11+ requires the MANAGE_EXTERNAL_STORAGE permission.

Add to android/app/src/main/AndroidManifest.xml:

<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />

Important: You must also request this permission at runtime using a package like permission_handler before calling any FileSaver API with a PathLocation or file:// URI on Android 11+.

Opening file:// URIs (PathLocation) #

If you save to a filesystem directory path (e.g. PathLocation(...)), the returned URI can be file://.... On Android, opening file:// URIs requires a FileProvider (Android N+ blocks sharing raw file:// URIs).

Only add this if you call FileSaver.openFile(uri) with file://... URIs.

Add to android/app/src/main/AndroidManifest.xml:

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.file_saver_ffi.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_saver_ffi_file_paths" />
</provider>

Create android/app/src/main/res/xml/file_saver_ffi_file_paths.xml:

<?xml version="1.0" encoding="utf-8"?>
<paths>
    <files-path name="files" path="." />
    <cache-path name="cache" path="." />
    <external-files-path name="external_files" path="." />
    <external-cache-path name="external_cache" path="." />
    <!-- Include this if you save to public external storage paths -->
    <external-path name="external" path="." />
</paths>
iOS Configuration

Supported: iOS 13.4+ (Swift Package Manager and CocoaPods)

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/>
MacOS Configuration

Supported: macOS 10.15.4+ (Swift Package Manager and CocoaPods)

Add to macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:

<!-- Required for network downloads -->
<key>com.apple.security.network.client</key>
<true/>

<!-- Required for directory picker (NSOpenPanel) -->
<key>com.apple.security.files.user-selected.read-write</key>
<true/>

<!-- Add entitlements for each MacosSaveLocation you use -->
<key>com.apple.security.files.downloads.read-write</key>
<true/>
<key>com.apple.security.assets.pictures.read-write</key>
<true/>
<key>com.apple.security.assets.movies.read-write</key>
<true/>
<key>com.apple.security.assets.music.read-write</key>
<true/>

Note: Each MacosSaveLocation requires its corresponding entitlement in sandboxed apps. Only MacosSaveLocation.documents (App Container) works without any entitlement. Use pickDirectory() to let users choose directories outside the sandbox.

Windows Configuration

Supported: Windows 10+

No configuration needed. Files are saved directly to Windows Known Folders (Downloads, Pictures, Videos, Music, Documents).

Linux Configuration

Supported: Ubuntu 20.04+ and other modern Linux distributions

Enable Linux desktop support on your development machine:

flutter config --enable-linux-desktop

Install required system packages (if not already present):

sudo apt-get install -y \
  clang cmake ninja-build \
  libgtk-3-dev pkg-config \
  libblkid-dev liblzma-dev

No additional app configuration needed. Directories are resolved via the XDG Base Directory Specification (xdg-user-dirs).

Web Configuration

Supported: All modern browsers (Chrome / Edge 86+ recommended)

Browser capabilities:

Feature Chrome / Edge 86+ Firefox Safari
save / saveNetwork ✅ ✅ ✅
saveAs (directory picker) ✅ File System Access API ❌ falls back to browser download ❌ falls back to browser download
Download progress ✅ saveAs + SaveNetworkInput, and saveNetwork with headers ❌ ❌

Download mechanism:

API Scenario Mechanism CORS required Progress
saveNetwork No custom headers <a href=url download> — browser native streaming ❌ ❌
saveNetwork With custom headers window.fetch() → Blob → <a download> ✅ ✅ (fraction needs Content-Length)
saveAs Chrome / Edge 86+ FSA: streams chunks directly to disk (zero RAM) ✅ ✅ (fraction needs Content-Length)
saveAs Firefox / Safari Falls back to standard browser download — —

Note: saveAs with directory selection requires the app to be served over HTTPS or localhost. http:// origins cannot use showDirectoryPicker.

See Web – Limitations for CORS, memory usage, and browser-specific constraints.

Basic Usage #

import 'package:file_saver_ffi/file_saver_ffi.dart';

try {
  final uri = await FileSaver.saveAsync(
    input: SaveInput.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}');
}

Core Concepts #

Unified API #

The library provides a single, consistent API for all save operations using SaveInput polymorphism:

  • save: Stream-based save to standard location (Downloads, Photos, etc.).
  • saveAsync: Future-based save to standard location.
  • saveAs: Stream-based save to a user-selected location (System Picker).
  • saveAsAsync: Future-based save to a user-selected location.

Input Sources (SaveInput)

Use the appropriate input class for your data source:

Input Type Data Source Use Case
SaveBytesInput Uint8List Small files in memory (images, generated PDFs)
SaveFileInput String (path) Large files from disk (videos, recordings)
SaveNetworkInput String (URL) Download and save directly from internet

Web: SaveFileInput is not supported and throws InvalidInputException. Use SaveBytesInput or SaveNetworkInput instead.

Usage Matrix 📊

Standard Location
(Downloads, Photos, etc.)
User-Chosen Location
(System Picker)
Advanced Control
(Stream, Cancel)
save() saveAs()
Simple / Await
(Future)
saveAsync() saveAsAsync()

Standard Location: defined enum (e.g., Downloads, Photos). User-Chosen: via pickDirectory() or auto-prompt.

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 using platform-specific enum values:

Platform Comparison

Value Android (AndroidSaveLocation) iOS (IosSaveLocation) macOS (MacosSaveLocation) Windows (WindowsSaveLocation) Linux (LinuxSaveLocation)
.downloads Downloads/ (default) - Downloads/ (default) Downloads/ (default) ~/Downloads/ (default)
.pictures Pictures/ - Pictures/ Pictures/ ~/Pictures/
.movies Movies/ - Movies/ - -
.videos - - - Videos/ ~/Videos/
.music Music/ - Music/ Music/ ~/Music/
.dcim DCIM/ - - - -
.documents - Documents/ (default) Documents/ Documents/ ~/Documents/
.photos - Photos Library - - -

Android 10+ file types per location: Android decides which file types a directory accepts. Images go to Pictures/ or DCIM/, videos to Movies/ or DCIM/ (Pictures/ too on newer versions: Android 16 accepts it, Android 10 doesn't), audio to Music/, and any type to Downloads/. Other combinations, such as a PDF in Pictures/, fail with InvalidInputException. Android 9 and below accept any type anywhere. A subDir containing .. is rejected.

Custom Paths (PathLocation): You can also pass PathLocation('/your/custom/path') anywhere a SaveLocation is expected, to save files directly to specific filesystem directory paths across all platforms (except Web). Note: On iOS, PathLocation only supports the app's document sandbox due to iOS system limitations.

Web: Platform-specific SaveLocation enums are not available. The saveLocation parameter in save/saveAsync is ignored — the browser controls the download destination (usually the Downloads folder). To save to a specific directory, use saveAs with pickDirectory() (requires Chrome / Edge 86+).

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 #

final uri = await FileSaver.saveAsync(
  input: SaveNetworkInput(
    url: 'https://example.com/video.mp4',
    headers: {'Authorization': 'Bearer token'}, // Optional headers
    timeout: Duration(minutes: 1), // Custom timeout
  ),
  fileName: 'downloaded_video',
  fileType: VideoType.mp4,
  saveLocation: switch (defaultTargetPlatform) {
    TargetPlatform.android => AndroidSaveLocation.movies,
    TargetPlatform.iOS => IosSaveLocation.photos,
    TargetPlatform.macOS => MacosSaveLocation.downloads,
    TargetPlatform.windows => WindowsSaveLocation.downloads,
    TargetPlatform.linux => LinuxSaveLocation.downloads,
    _ => null,
  },
);

Progress Tracking #

// Using Unified API
await FileSaver.saveAsync(
  input: SaveNetworkInput(url: '...'),
  fileName: 'video',
  fileType: VideoType.mp4,
  onProgress: (progress) {
    print('Download progress: ${(progress * 100).toInt()}%');
  },
);

// Using the stream, which also gives the bytes written. A download without
// Content-Length (or a compressed one) has an unknown size: totalBytes is
// null and progress stays 0.0.
FileSaver.save(
  input: SaveNetworkInput(url: '...'),
  fileName: 'video',
  fileType: VideoType.mp4,
).listen((event) {
  if (event case SaveProgressUpdate(:final bytesWritten, :final totalBytes)) {
    print(totalBytes == null
        ? '$bytesWritten bytes'
        : '$bytesWritten of $totalBytes bytes');
  }
});

Cancellation #

// Stream API allows cancellation
final subscription = FileSaver.save(
  input: SaveNetworkInput(url: '...'), // Works for all inputs
  fileName: 'video',
  fileType: VideoType.mp4,
).listen((event) => print(event));

// Cancel anytime. Completes once the partial file is deleted.
await subscription.cancel();

A cancelled stream emits no further events, so no SaveProgressCancelled or CancelledException follows. Leaving an await for loop early cancels the save the same way.

Save to User-Selected Directory #

// 1. Pick directory (Optional, saveAs handles this automatically if null)
final location = await FileSaver.pickDirectory();

if (location != null) {
  // 2. Save file to that directory
  await FileSaver.saveAsAsync(
    input: SaveBytesInput(pdfBytes),
    fileName: 'invoice',
    fileType: CustomFileType(ext: 'pdf', mimeType: 'application/pdf'),
    saveLocation: location,
  );
}

To use the same directory after the app restarts, store a token from persistDirectory and restore it on the next launch. On Android the token keeps a persistable URI permission, on iOS and macOS it holds bookmark data, and on web it refers to the directory handle stored in IndexedDB. On web a restored directory may need requestAccess() from a button first; see WEB_LIMITATIONS.md.

// After picking:
prefs.setString('exportDir', await FileSaver.persistDirectory(location));

// On a later launch:
final token = prefs.getString('exportDir');
final restored = token == null ? null : await FileSaver.restoreDirectory(token);
if (restored == null) {
  // Access is gone (directory deleted, permission revoked): pick again.
} else {
  // A bookmark goes stale when the directory moves, so store a fresh token.
  prefs.setString('exportDir', await FileSaver.persistDirectory(restored));
}

// When the directory is no longer needed:
await FileSaver.releaseDirectory(prefs.getString('exportDir')!);
await prefs.remove('exportDir');

API Reference #

Unified API #

save

Stream-based API for advanced control (cancellation, detailed events).

Stream<SaveProgress> save({
  required SaveInput input,
  required String fileName,
  required FileType fileType,
  // ... same optional params
})

saveAsync

Future-based API for simple usage.

Future<Uri> saveAsync({
  required SaveInput input,
  required String fileName,
  required FileType fileType,
  SaveLocation? saveLocation,
  String? subDir,
  ConflictResolution conflictResolution,
  Function(double)? onProgress,
})

saveAs

Stream-based interactive save.

Stream<SaveProgress> saveAs({
  required SaveInput input,
  required String fileName,
  required FileType fileType,
  PickedDirectoryLocation? saveLocation,
  ConflictResolution conflictResolution,
})

saveAsAsync

Interactive save (shows picker) or save to specific PickedDirectoryLocation. Returns null when the picker is dismissed. Invalid input, such as empty bytes, throws InvalidInputException before the picker opens.

Future<Uri?> saveAsAsync({
  required SaveInput input,
  required String fileName,
  required FileType fileType,
  PickedDirectoryLocation? saveLocation,
  ConflictResolution conflictResolution,
  Function(double)? onProgress,
})

openWrite

Opens a file for streaming writes, so the data can be written chunk by chunk. Returns a FileSaverSink, or null when conflictResolution is skip and the file exists.

Future<FileSaverSink?> openWrite({
  required String fileName,
  required FileType fileType,
  SaveLocation? saveLocation,
  String? subDir,
  int? totalSize,
  ConflictResolution conflictResolution,
})

close() finishes the file and returns its URI. cancel() deletes the partial file. If a write fails, close() throws that error and the partial file is deleted.

final sink = await FileSaver.openWrite(
  fileName: 'recording',
  fileType: VideoType.mp4,
  totalSize: expectedBytes,
);
if (sink == null) return;
sink.progress.listen((p) => print('${(p * 100).round()}%'));
await sink.addStream(chunks); // waits for each chunk to be written
final uri = await sink.close();

openWriteAs

Streaming writes to a user-selected directory (the system picker is shown when saveLocation is null). Returns a FileSaverSink, or null when the picker is cancelled or skip finds an existing file.

Future<FileSaverSink?> openWriteAs({
  required String fileName,
  required FileType fileType,
  PickedDirectoryLocation? saveLocation,
  int? totalSize,
  ConflictResolution conflictResolution,
})

pickDirectory

Open system picker to let user choose a folder. shouldPersist (Android only) is deprecated and will be removed in 1.0.0: call persistDirectory on the result instead.

Future<PickedDirectoryLocation?> pickDirectory({bool shouldPersist = true})

persistDirectory / restoreDirectory / releaseDirectory

Keep access to a picked directory across app restarts.

static Future<String> persistDirectory(PickedDirectoryLocation location)
static Future<PickedDirectoryLocation?> restoreDirectory(String token)
static Future<void> releaseDirectory(String token)

canOpenFile

Checks whether the file at uri is accessible for reading.

static Future<bool> canOpenFile(Uri uri)

openFile

Opens a saved file with the appropriate system app.

static Future<void> openFile(Uri uri, {String? mimeType})

Input Models #

SaveBytesInput

  • fileBytes: Uint8List (Required)

SaveFileInput

  • filePath: String (Required - absolute path)

SaveNetworkInput

  • url: String (Required)
  • headers: Map<String, String>? (Optional)
  • timeout: Duration (Default: 60s) — enforced on Web via AbortController when custom headers are used; ignored for header-less requests (anchor element)

SaveProgress Events #

Stream API emits these sealed class events. A save stream is single-subscription: the save starts when it is listened to, and a second listen throws a StateError instead of saving the file again. Call save again for another save, or use asBroadcast() to share one save between listeners.

Event Properties Description
SaveProgressStarted - Operation began
SaveProgressUpdate progress: double, bytesWritten: int, totalBytes: int? Progress from 0.0 to 1.0, and the bytes written of the file's size (null if unknown)
SaveProgressComplete uri: Uri Success with saved file URI
SaveProgressError exception: FileSaverException Error occurred
SaveProgressCancelled - saveAs picker dismissed

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
InvalidInputException Empty bytes or invalid input INVALID_INPUT
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
NetworkException Download failed; statusCode is set for an HTTP error NETWORK_ERROR
OpenFailedException openFile found no app or failed OPEN_FAILED
CancelledException close() of a cancelled FileSaverSink CANCELLED
NativePlatformException Generic platform error, or a dir_picker error with its code PLATFORM_ERROR

FileSaverErrorCode has a constant for each code, plus INCOMPLETE, which saveAsync and saveAsAsync throw as a NativePlatformException when the stream ends without a result.

Contributing #

Contributions are welcome! Please feel free to submit a Pull Request.

FAQ #

â„šī¸ iOS Photos Permissions & Albums

When saving to IosSaveLocation.photos, the permission requested depends on the subDir:

subDir Permission Dialog (iOS 14+) Capabilities
"MyAlbum" .readWrite Full / Limited / Deny Album creation, conflict resolution
null .addOnly Allow / Deny Basic save only (no album)

If the app already has .readWrite access (full or limited), a save without subDir uses it too, so conflict resolution works and canOpenFile/openFile accept the returned ph:// URI. With .addOnly access, existing assets can't be looked up: every save adds a new asset, and the URI can't be opened.

Important: If .readWrite is denied, the save fails. There is no automatic fallback to .addOnly.

Only images and videos can be saved to IosSaveLocation.photos. Audio and other files throw InvalidInputException with every API; save them to IosSaveLocation.documents.

❓ Why are files not saving to Downloads, Pictures, Music (macOS)?

App Sandbox restricts access to user folders by default. If your app is sandboxed (which is typical for macOS Store apps), you must add specific entitlements to your .entitlements files.

Solution: Add the required keys (e.g., com.apple.security.files.downloads.read-write) to your macos/Runner/*.entitlements files.

Check the MacOS Configuration section above for the full list of required keys.

License #

MIT License - see LICENSE file for details.

14
likes
160
points
3.17k
downloads
screenshot

Documentation

API reference

Publisher

verified publisherv4g.space

Weekly Downloads

Save bytes, files and downloads to the gallery, device storage or a user-picked folder, with progress, cancellation and conflict handling. Native FFI and JNI on mobile.

Repository (GitHub)
View/report issues

Topics

#file-saver #gallery-saver #saver #ffi #jni

License

MIT (license)

Dependencies

dir_picker, ffi, flutter, flutter_web_plugins, jni, path, path_provider_windows, web

More

Packages that depend on file_saver_ffi

Packages that implement file_saver_ffi