Skip to content

jvoltci/saf

Repository files navigation

saf — one class for the Android Storage Access Framework

Saf CI pipeline status

Saf

Scoped storage broke File('/storage/emulated/0/…'). saf is the fix.

One package for the Android Storage Access Framework: pickers, persisted permissions, file management with recursive walk, streamed read/write with progress, thumbnails, raw file descriptors, and local-file bridging — all in a single Saf class. Ask the user once, keep the grant forever, and work with their files in a handful of lines instead of hundreds of lines of platform code.

flutter pub add saf

Highlights

  • One class, no ceremony — pickers, permissions, file management, and I/O all on Saf; no isDir parameters anywhere.
  • Typed errorsSafPermissionException, SafNotFoundException, SafAlreadyExistsException, SafIoException.
  • Recursive walk() plus recursive copyTo / moveTo with progress callbacks.
  • Streaming I/O — backpressured readFileStream and one-call writeFileStream for large files.
  • File descriptorsopenFileDescriptor / withFileDescriptor hand native or path-based APIs a live /proc/self/fd/<fd>.
  • Thumbnailsthumbnail() returns provider-generated JPEG bytes for images and videos.
  • 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 with copyDirToLocal.
  • Broad support — Dart ≥ 3.0, Flutter ≥ 3.10, Android minSdk 21.

Quick start

import 'dart:convert';
import 'dart:typed_data';
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}'));

// 5. Thumbnails for a gallery grid — provider-generated, no full decode.
final jpeg = await saf.thumbnail(doc.uri, 256, 256, 80); // Uint8List? for Image.memory

// 6. Hand a SAF file to anything that wants a real path or fd —
//    video players, PDF renderers, sqlite — no copy, auto-closed.
final title = await saf.withFileDescriptor(doc.uri, 'r', (fd) async {
  return someNativeLib.readMetadata(fd.path); // /proc/self/fd/<fd>
});

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. Raw file descriptors and provider thumbnails now ship (see openFileDescriptor / withFileDescriptor and thumbnail). Intentionally out of scope for now:

  • Media picking — use image_picker / photo_manager, which specialize in it.

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.

See the architecture page for layer diagrams and a grant-then-read sequence.

Try it in 30 seconds

The bundled example is a mini file manager: pick a folder, browse it with live image thumbnails, tap a file for its details and file-descriptor path, write a file back. cd example && flutter run.

Quality

  • Tested — full unit-test suite on the Dart layer; exercised end-to-end on physical devices.
  • Hardened — the file layer is written defensively: an aborted write never deletes a pre-existing file, recursive copy refuses to recurse into itself, and every picker/stream failure path cleans up after itself.
  • Typed & mockable — sealed exceptions and a platform-interface layer you can fake in your own tests.

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.

About

Flutter plugin that leverages Storage Access Framework (SAF) API to get access and perform the operations on files and folders.

Topics

Resources

License

Contributing

Stars

25 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors