Real files on Flutter Web. Save in place instead of downloading a copy, and reopen the same file after a reload with the File System Access API.
Real files on Flutter Web.
final file = await FileSystemAccess.saveFile(suggestedName: 'notes.txt');await file?.writeText('saved into the file the user picked');// Later, after a full page reload:final again = await FileSystemAccess.recallFile('last-document');print(await again?.readText()); // the same file, still thereOn Flutter Web, "Save" means "download another copy". The user picks
notes.txt, you write it, and their Downloads folder gains notes (1).txt,
then notes (2).txt. There is no way to write back into the file they opened,
and no way to find it again after a reload.
The File System Access API fixes both, and Flutter has no binding for it.
package:web ships the handle types but not the pickers — they appear only in
its doc comments — and neither permissions nor directory iteration.
final files = await FileSystemAccess.openFiles( types: [FilePickerType.mime('text/plain', ['.txt', '.md'])],);final file = files.first;final body = await file.readText();await file.writeText('$body\n\nedited'); // same file, no second copyCancelling a picker is not an error — openFiles returns an empty list and
saveFile returns null, because the user changing their mind is not an
exceptional condition.
Handles are structured cloneable, so they can be kept in IndexedDB and taken
out on the next visit. This is the part that is genuinely hard without the API:
await FileSystemAccess.remember('last-document', file);// next visitfinal file = await FileSystemAccess.recallFile('last-document');if (await file?.permission(write: true) != FilePermission.granted) { await file?.requestPermission(write: true); // needs a user gesture}The browser may still confirm write access on a new visit. That is its
decision, not this package's, and permission tells you which state you are in
before you try.
final s = FileSystemAccess.support;s.openPicker; // Chrome, Edges.savePicker;s.directoryPicker;s.originPrivate; // far more widely available
Four questions rather than one boolean, because the answers really do differ:
Safari has the origin-private file system and no pickers at all. Code that
checks a single flag will show a Save button that cannot work.
final dir = await FileSystemAccess.openDirectory(write: true);for (final entry in await dir!.list()) { print('${entry.name} ${entry.isDirectory ? "dir" : "file"}');}await dir.file('report.txt', create: true);await dir.remove('old.txt');final root = await FileSystemAccess.originPrivateDirectory();final scratch = await root.file('cache.bin', create: true);The origin-private file system needs no picker and no permission, and is
supported far more widely than the pickers. It is invisible to the user, so use
it for scratch data rather than for anything they should be able to find on
disk.
The package compiles everywhere. On mobile and desktop isSupported is false
and every call throws UnsupportedByBrowserException with a message pointing
at the check you should have made — so an app that also targets web can depend
on it without conditional imports of its own.
The web path is tested in a real browser, not mocked: `flutter test --platform
chrome` covers byte-for-byte round trips, non-ASCII text, truncation on
overwrite, directory listing, error paths, and storing a handle in IndexedDB
and reading the file back through it. The stub path is tested on the VM.
MIT © K M Shahriar Hossain