Interface to provide a way to save files on the device in Flutter.
| Android | iOS | Web | Windows | Linux | MacOS | |
|---|---|---|---|---|---|---|
writeFileAsBytes |
✅ | ✅ | ✅ | ❌️ | ❌️ | ✅ |
writeFileAsString |
✅ | ✅ | ✅ | ❌️ | ❌️ | ✅ |
Under the hood, each implementation tries to use the native dialog to save the file:
- Android:
ACTION_CREATE_DOCUMENTintent is used - iOS:
UIDocumentPickerViewControlleris used - MacOS:
NSSavePanelis used - Web: It will try to use the
showSaveFilePickerAPI if available, otherwise it will try to use thedownloadattribute on an anchor tag
dependencies:
flutter_file_saver: anyiOS Setup (Needs iOS 16+)
Add the following permissions to your ios/Runner/Info.plist:
<key>UISupportsDocumentBrowser</key>
<true/>
<key>UIFileSharingEnabled</key>
<true/>
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>MacOS Setup
Add the following permissions to your macos/Runner/DebugProfile.entitlements:
<key>com.apple.security.files.user-selected.read-write</key>
<true/>Write a file on the device from a Uint8List.
FlutterFileSaver().writeFileAsBytes(
fileName: 'file.txt',
bytes: fileBytes,
);Write a file on the device from a String. This will most of the time convert your data and perform a call to writeFileAsBytes.
FlutterFileSaver().writeFileAsString(
fileName: 'file.txt',
string: 'Hello World!',
);This is a known issue with the showSaveFilePicker JavaScript API on the Web. It is caused when writeFileAsBytes/writeFileAsString is not immediately called after a user interaction (like a button click). This is a security feature of the browser to prevent malicious sites from opening file pickers without user consent.
ElevatedButton(
onPressed: () async {
final bytes = await compressFile();
// Might throw a SecurityError if compressFile() has taken too long to execute.
FlutterFileSaver().writeFileAsBytes(bytes: bytes, fileName: myFileName);
},
child: const Text('Save File'),
),The fix is to call writeFileAsBytes/writeFileAsString immediately after a user interaction. For example, by having a two-phase save flow with a button specifically made to save the file:
ElevatedButton(
onPressed: () async {
// 1st phase: compress the file
final bytes = await compressFile();
if (!context.mounted) return;
// 2nd phase: show a dialog to save the file
showDialog(context: context, builder: (_) => DownloadDialog(bytes: bytes, fileName: myFileName));
},
child: const Text('Save File'),
),
class DownloadDialog extends StatelessWidget {
const DownloadDialog({super.key, required this.bytes, required this.fileName});
final Uint8List bytes;
final String fileName;
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Save File'),
content: const Text('Do you want to save the file?'),
actions: [
TextButton(
onPressed: () {
// Will work because it is called immediately after a user interaction.
FlutterFileSaver().writeFileAsBytes(bytes: bytes, fileName: fileName);
Navigator.pop(context);
},
child: const Text('Save'),
),
],
);
}
}Check the example