public_file_saver 1.0.0
public_file_saver: ^1.0.0 copied to clipboard
A cross-platform Flutter plugin to save files to publicly visible locations (Downloads, Documents). Supports Android, iOS, and HarmonyOS.
public_file_saver #
A cross-platform Flutter plugin to save files to publicly visible locations (Downloads, Documents). Supports Android, iOS, and HarmonyOS (OHOS).
中文文档
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
Platform Support #
| Feature | Android | iOS | HarmonyOS (OHOS) |
|---|---|---|---|
saveBytes() |
✅ | ✅ | ✅ |
saveBytesWithDialog() |
✅ | ✅ | ✅ |
saveFile() |
✅ | ✅ | ✅ |
saveFromUrl() |
✅ | ✅ | ✅ |
subDir parameter |
✅ | ✅ | ❌ |
fileSuffixChoices parameter |
❌ | ❌ | ✅ |
Installation #
Add this to your pubspec.yaml:
dependencies:
public_file_saver: ^1.0.0
Then run:
flutter pub get
Platform-Specific Setup #
Android #
No additional setup required. The plugin handles permissions automatically:
- Android 10+ (API 29+): Uses MediaStore API, no permission needed
- Android 9 and below: Uses public Downloads directory
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/>
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 only
})
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 Downloads (Android only) |
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 |
| OHOS | User-selected via DocumentViewPicker | uri and path |
Example:
final bytes = Uint8List.fromList(utf8.encode('Hello, World!'));
final result = await fileSaver.saveBytes(
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 |
| 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.saveBytesWithDialog(
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 only) |
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.saveFile(
file: file,
subDir: 'Documents',
);
// Save with dialog
final result = await fileSaver.saveFile(
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,
})
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 only) |
useDialog |
bool |
No | If true, shows file picker dialog after download |
Example:
// Download and save directly
final result = await fileSaver.saveFromUrl(
url: 'https://example.com/document.pdf',
subDir: 'Downloads',
);
// Download and show save dialog
final result = await fileSaver.saveFromUrl(
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 |
| OHOS | File URI | Converted path |
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.saveBytes(
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.saveBytesWithDialog(
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.saveFromUrl(
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.saveFromUrl(url: 'https://example.com/file.pdf');
} catch (e) {
print('Download failed: $e');
}
License #
MIT License - see LICENSE file for details.
Contributing #
Contributions are welcome! Please read the contributing guidelines before submitting a PR.