Skip to main content

public_file_saver 1.2.1 copy "public_file_saver: ^1.2.1" to clipboard
public_file_saver: ^1.2.1 copied to clipboard

A reliable Flutter plugin for saving bytes, local files, and URL downloads to user-visible locations with direct-save and native-dialog support.

New API: use save() for bytes, local files, and URLs. The four legacy methods below are deprecated but remain available.

public_file_saver #

A reliable Flutter plugin for saving bytes, local files, and URL downloads to user-visible locations. It supports both direct saves and native save dialogs across Android, iOS, macOS, Web, Windows, Linux, and HarmonyOS (OHOS).

pub package

中文文档

Features #

  • ✅ Save binary data (Uint8List) to public directories
  • ✅ Save with system file picker dialog
  • ✅ Save local files
  • ✅ Download and save files from URL
  • ✅ Automatic file name sanitization
  • ✅ MIME type inference
  • ✅ Consistent return format across all platforms
  • ✅ Swift Package Manager support on iOS

Platform Support #

Feature Android iOS macOS Web Windows Linux OHOS
saveBytes() ✅ ✅ ✅ ✅¹ ✅ ✅ ✅
saveBytesWithDialog() ✅ ✅ ✅ ✅¹ ✅ ✅ ✅
saveFile() ✅ ✅ ✅ ❌² ✅ ✅ ✅
saveFromUrl() ✅ ✅ ✅ ✅¹ ✅ ✅ ✅
subDir parameter ✅ ✅ ✅ ❌ ✅ ✅ ❌
fileSuffixChoices parameter ❌ ❌ ❌ ❌ ❌ ❌ ✅

¹ On Web, saveBytesWithDialog() uses the native Save As picker in browsers that support the File System Access API (such as Chrome and Edge). Other browsers fall back to a normal download. The returned PublicSavedFile only carries fileName because browsers do not expose the final path.

² On Web, calling saveFile(File) throws UnsupportedError — dart:io's File is not available in the browser. Read the bytes yourself and call saveBytes() instead.

Installation #

Add this to your pubspec.yaml:

dependencies:
  public_file_saver: ^1.2.0

Then run:

flutter pub get

Platform-Specific Setup #

Android #

The plugin declares the WRITE_EXTERNAL_STORAGE permission (limited to Android 9 and below via android:maxSdkVersion="28"), so it is merged into your app automatically.

  • Android 10+ (API 29+): Uses MediaStore API, no permission needed
  • Android 6.0–9: The plugin declares the permission, but your app must request it at runtime (e.g. with the permission_handler package) before calling saveBytes()
  • Android 5.1 and below: Permission is granted automatically at install time

iOS #

Add the following keys to your Info.plist if you want users to access saved files via the Files app:

<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>

macOS #

If your app is sandboxed (the default for Mac App Store builds), enable user-selected file access in your entitlements to allow the save dialog to write outside the app sandbox:

<key>com.apple.security.files.user-selected.read-write</key>
<true/>

Without the sandbox, no extra entitlements are needed. Direct (no-dialog) saveBytes writes to ~/Downloads; in a sandboxed build, the system redirects this to the per-app container's Downloads folder unless you add the Downloads folder entitlement to write to the real ~/Downloads:

<key>com.apple.security.files.downloads.read-write</key>
<true/>

Web #

No setup is required. saveBytes() triggers a standard browser download. saveBytesWithDialog() opens the native Save As picker when the File System Access API is available (secure contexts and localhost); unsupported browsers fall back to a standard download.

Windows #

No setup required. Direct mode uses FOLDERID_Downloads (the user's standard Downloads folder); dialog mode uses the native IFileSaveDialog.

Linux #

The plugin uses GTK 3, which is already linked by every Flutter Linux app, so no extra dependencies are required. Direct mode resolves the XDG Downloads directory (g_get_user_special_dir(G_USER_DIRECTORY_DOWNLOAD)), falling back to $HOME if XDG is not configured; dialog mode uses GtkFileChooserNative.

HarmonyOS (OHOS) #

The plugin uses DocumentViewPicker which requires no additional permissions.

Usage #

Import #

import 'package:public_file_saver/public_file_saver.dart';

Create Instance #

final fileSaver = PublicFileSaver();

API Reference #


saveBytes()

Save binary data directly to a public location without showing a dialog.

Future<PublicSavedFile?> saveBytes({
  required Uint8List bytes,
  required String fileName,
  String mimeType = 'application/octet-stream',
  String? subDir,  // Android, iOS, macOS, Windows, Linux
})

Parameters:

Parameter Type Required Description
bytes Uint8List Yes The binary data to save
fileName String Yes Desired file name (will be sanitized)
mimeType String No MIME type of the file (default: application/octet-stream)
subDir String? No Subdirectory within the public directory (Android, iOS, macOS, Windows, Linux)

Platform Behavior:

Platform Save Location Returns
Android 10+ MediaStore Downloads uri: content:// URI
Android 9- Public Downloads directory path: full file path
iOS App Documents directory (visible in Files app) path: full file path
macOS ~/Downloads (or sandbox-redirected) uri (file://) and path
Web Browser download (browser-chosen) fileName only
Windows FOLDERID_Downloads path: full file path
Linux XDG $HOME/Downloads uri (file://) and path
OHOS User-selected via DocumentViewPicker uri and path

Example:

final bytes = Uint8List.fromList(utf8.encode('Hello, World!'));

final result = await fileSaver.save(
  bytes: bytes,
  fileName: 'hello.txt',
  mimeType: 'text/plain',
  subDir: 'MyApp', // Creates Downloads/MyApp/hello.txt on Android
);

if (result != null && result.isSuccess) {
  print('Saved: ${result.fileName}');
  print('URI: ${result.uri}');
  print('Path: ${result.path}');
}

saveBytesWithDialog()

Save binary data with a system file picker dialog, allowing users to choose the save location.

Future<PublicSavedFile?> saveBytesWithDialog({
  required Uint8List bytes,
  required String fileName,
  String mimeType = 'application/octet-stream',
  List<String>? fileSuffixChoices,  // OHOS only
})

Parameters:

Parameter Type Required Description
bytes Uint8List Yes The binary data to save
fileName String Yes Suggested file name
mimeType String No MIME type of the file
fileSuffixChoices List<String>? No File extension choices (OHOS only)

Platform Behavior:

Platform Dialog Type Returns
Android Storage Access Framework (ACTION_CREATE_DOCUMENT) uri: content:// URI
iOS UIDocumentPickerViewController uri: file:// URL, path: file path
macOS NSSavePanel uri (file://) and path
Web Native Save As picker when supported; otherwise browser download fileName only
Windows IFileSaveDialog (COM) path: full file path
Linux GtkFileChooserNative uri (file://) and path
OHOS DocumentViewPicker.save uri and path

Example:

final jsonData = {'name': 'Test', 'value': 123};
final bytes = Uint8List.fromList(
  utf8.encode(jsonEncode(jsonData))
);

final result = await fileSaver.save(useDialog: true,
  bytes: bytes,
  fileName: 'data.json',
  mimeType: 'application/json',
);

if (result != null && result.isSuccess) {
  print('User saved file to: ${result.path ?? result.uri}');
} else {
  print('User cancelled or save failed');
}

saveFile()

Save a local File object to a public location.

Future<PublicSavedFile?> saveFile({
  required File file,
  String? fileName,
  String? mimeType,
  String? subDir,
  bool useDialog = false,
})

Parameters:

Parameter Type Required Description
file File Yes The file to save
fileName String? No Custom file name (uses original name if not provided)
mimeType String? No MIME type (inferred from extension if not provided)
subDir String? No Subdirectory (non-dialog mode, Android, iOS, macOS, Windows, Linux)
useDialog bool No If true, shows file picker dialog

Example:

import 'dart:io';

final file = File('/path/to/document.pdf');

// Save without dialog
final result = await fileSaver.save(
  file: file,
  subDir: 'Documents',
);

// Save with dialog
final result = await fileSaver.save(
  file: file,
  fileName: 'renamed_document.pdf',
  useDialog: true,
);

saveFromUrl()

Download a file from a URL and save it to a public location.

Future<PublicSavedFile?> saveFromUrl({
  required String url,
  String? fileName,
  String? mimeType,
  String? subDir,
  bool useDialog = false,
  Duration? timeout,
})

Parameters:

Parameter Type Required Description
url String Yes HTTP(S) URL to download from
fileName String? No Custom file name (inferred from URL/headers if not provided)
mimeType String? No MIME type (inferred from Content-Type header if not provided)
subDir String? No Subdirectory (non-dialog mode, Android, iOS, macOS, Windows, Linux)
useDialog bool No If true, shows file picker dialog after download
timeout Duration? No Timeout for the HTTP request (throws TimeoutException when exceeded)

Example:

// Download and save directly
final result = await fileSaver.save(
  url: 'https://example.com/document.pdf',
  subDir: 'Downloads',
);

// Download and show save dialog
final result = await fileSaver.save(
  url: 'https://example.com/image.png',
  fileName: 'my_image.png',
  useDialog: true,
);

if (result != null && result.isSuccess) {
  print('Downloaded and saved: ${result.fileName}');
}

Return Type: PublicSavedFile #

All save methods return PublicSavedFile?:

class PublicSavedFile {
  final String fileName;  // Name of the saved file
  final String? uri;      // URI of the saved file (platform-dependent)
  final String? path;     // File system path (platform-dependent)

  bool get isSuccess => uri != null || path != null;
}

Return values by platform:

Platform uri path
Android 10+ content:// URI null
Android 9- null Full file path
Android (dialog) content:// URI null
iOS (direct) null Full file path
iOS (dialog) file:// URL Full file path
macOS file:// URL Full file path
Web null null (browser-controlled)
Windows null Full file path
Linux file:// URL Full file path
OHOS File URI Converted path

On Web, PublicSavedFile.isSuccess uses the non-empty file name because browsers do not expose the actual save path to JavaScript.

Utility Methods #

sanitizeFileName()

Sanitize file names by replacing illegal characters.

final safeName = PublicFileSaver.sanitizeFileName('file:name?.txt');
// Result: 'file_name_.txt'

Replaced characters: \ / : * ? " < > |

Complete Example #

import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:public_file_saver/public_file_saver.dart';

class SaveFileExample extends StatefulWidget {
  @override
  _SaveFileExampleState createState() => _SaveFileExampleState();
}

class _SaveFileExampleState extends State<SaveFileExample> {
  final _fileSaver = PublicFileSaver();
  String _status = 'Ready';

  Future<void> _saveTextFile() async {
    final bytes = Uint8List.fromList(
      utf8.encode('Hello from Flutter!\nTimestamp: ${DateTime.now()}'),
    );

    final result = await _fileSaver.save(
      bytes: bytes,
      fileName: 'flutter_demo.txt',
      mimeType: 'text/plain',
    );

    setState(() {
      if (result != null && result.isSuccess) {
        _status = 'Saved: ${result.fileName}\n'
                  'URI: ${result.uri}\n'
                  'Path: ${result.path}';
      } else {
        _status = 'Save failed or cancelled';
      }
    });
  }

  Future<void> _saveWithDialog() async {
    final data = {'message': 'Hello', 'timestamp': DateTime.now().toIso8601String()};
    final bytes = Uint8List.fromList(utf8.encode(jsonEncode(data)));

    final result = await _fileSaver.save(useDialog: true,
      bytes: bytes,
      fileName: 'data.json',
      mimeType: 'application/json',
    );

    setState(() {
      _status = result?.isSuccess == true
        ? 'Saved to: ${result!.path ?? result.uri}'
        : 'Cancelled';
    });
  }

  Future<void> _downloadAndSave() async {
    try {
      final result = await _fileSaver.save(
        url: 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf',
        useDialog: true,
      );

      setState(() {
        _status = result?.isSuccess == true
          ? 'Downloaded: ${result!.fileName}'
          : 'Failed';
      });
    } catch (e) {
      setState(() {
        _status = 'Error: $e';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(_status),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _saveTextFile,
          child: Text('Save Text File'),
        ),
        ElevatedButton(
          onPressed: _saveWithDialog,
          child: Text('Save with Dialog'),
        ),
        ElevatedButton(
          onPressed: _downloadAndSave,
          child: Text('Download & Save'),
        ),
      ],
    );
  }
}

Error Handling #

All methods return null when:

  • User cancels the save dialog
  • Save operation fails
  • Required parameters are missing

For saveFromUrl(), network errors will throw exceptions:

try {
  final result = await fileSaver.save(url: 'https://example.com/file.pdf');
} catch (e) {
  print('Download failed: $e');
}

Unified save API and migration #

Use PublicFileSaver().save(...) with exactly one of bytes, file, or url:

final saver = PublicFileSaver();
await saver.save(bytes: bytes, fileName: 'report.pdf');
await saver.save(file: file, useDialog: true);
await saver.save(
  url: 'https://example.com/report.pdf',
  headers: {'Authorization': 'Bearer token'},
  timeout: const Duration(seconds: 30),
);

Bytes require a nonblank fileName. MIME type is inferred unless supplied. Use subDir only for direct saves and fileSuffixChoices only with a dialog (suffix choices are currently OHOS-specific). URL headers and timeout require a URL source. Invalid combinations throw ArgumentError before I/O.

saveBytes, saveBytesWithDialog, saveFile, and saveFromUrl are deprecated but retain their previous behavior. Replace their calls with save; for saveBytesWithDialog, also pass useDialog: true. Local files remain unsupported on Web. Files and downloads are still buffered in memory; timeout does not guarantee cancellation of the underlying request. Browser save dialogs retain their existing browser support and user-gesture requirements.

License #

MIT License - see LICENSE file for details.

Contributing #

Contributions are welcome! Please read the contributing guidelines before submitting a PR.

2
likes
145
points
1.8k
downloads

Documentation

API reference

Publisher

verified publisherchihero.com

Weekly Downloads

A reliable Flutter plugin for saving bytes, local files, and URL downloads to user-visible locations with direct-save and native-dialog support.

Repository (GitHub)
View/report issues

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, http, mime, plugin_platform_interface

More

Packages that depend on public_file_saver

Packages that implement public_file_saver