Saf
One package for the Android Storage Access Framework: pickers, persisted
permissions, file management with recursive walk, streamed read/write with
progress, and local-file bridging — all in a single Saf class.
Highlights
- One class, no ceremony — pickers, permissions, file management, and I/O all on
Saf; noisDirparameters anywhere. - Typed errors —
SafPermissionException,SafNotFoundException,SafAlreadyExistsException,SafIoException. - Recursive
walk()plus recursivecopyTo/moveTowith progress callbacks. - Streaming I/O — backpressured
readFileStreamand one-callwriteFileStreamfor large files. - Persisted permissions — grant once, reuse across restarts; list them with
persistedPermissions(). - Hidden folders — read dotfile folders (e.g. WhatsApp
.Statuses) and pull them into your app dir withcopyDirToLocal. - Broad support — Dart ≥ 3.0, Flutter ≥ 3.10, Android minSdk 21.
Quick start
import 'package:saf/saf.dart';
final saf = Saf();
// 1. Ask once — the grant persists across restarts.
final dir = await saf.pickDirectory();
if (dir == null) return; // user cancelled
// Later launches: reuse the grant instead of prompting again.
final grants = await saf.persistedPermissions();
// 2. Manage files.
final files = await saf.list(dir.uri);
final report = await saf.mkdirp(dir.uri, ['reports', '2026']);
await for (final entry in saf.walk(dir.uri)) {
print(entry.relativePath);
}
// 3. Read and write.
final doc = await saf.writeFileBytes(
report.uri, 'summary.txt', 'text/plain', utf8.encode('hi') as Uint8List);
final bytes = await saf.readFileBytes(doc.uri);
final stream = await saf.readFileStream(doc.uri); // large files
// 4. Bridge to real file paths when another API needs one.
await saf.copyToLocalFile(doc.uri, '${cacheDir.path}/summary.txt',
onProgress: (p) => print('${p.bytesDone}/${p.totalBytes}'));
Errors are typed — catch what you care about:
try {
await saf.delete(uri);
} on SafPermissionException {
// re-pick the directory
} on SafNotFoundException {
// already gone
}
Migrating
From saf 1.x
The old path-based class still works as LegacySaf (deprecated, removed in
3.0.0): rename Saf( → LegacySaf( and migrate at your own pace. The new API
is URI-based — start from pickDirectory() and store URIs, not paths.
Scope
saf keeps a focused, purposeful API. Intentionally out of scope for now:
- Media picking — use
image_picker/photo_manager, which specialize in it. - Raw file descriptors and thumbnails — niche; open an issue if you need them and they can land in a 2.1+.
Architecture
saf is a single package with a mockable platform-interface layer, a thin Dart
facade, and one coroutine-based Kotlin handler on a dedicated channel — the
legacy 1.x channels are left untouched.
Flow: your app → Saf (facade) → SafPlatform → method channel → SafV2Api (Kotlin, coroutines) → DocumentsContract → Android SAF. All I/O runs off the main thread; the legacy 1.x channels are untouched.
See the architecture page for layer diagrams and a grant-then-read sequence.
Documentation
- Documentation site — full API reference.
- Saf class reference — every method.
Getting Started with Flutter
For help getting started with Flutter, view the online documentation.
Built & maintained by jvoltci · Docs · Issues · pub.dev · MIT License
⭐ If saf saves you time, star the repo — it helps other Flutter devs find it.