Skip to content

Storage Access Framework

Import package

import 'package:shared_storage/shared_storage.dart' as saf;

Usage sample:

saf.openDocumentTree(...);
saf.listFiles(...);

But if you import without alias import '...'; (Not recommeded because can conflict with other method/package names) you should use directly as functions:

openDocumentTree(...);
listFiles(...);

Example project

The example project does use of most of these APIs, that is available at /example

Concepts

This is a brief explanation of the core concepts of this API.

What's an Uri?

Uri is a the most confusing concept we can found. Since it's not a regular string, it's not a regular url, neither a regular file system path.

By the official docs:

A URI is a uniform resource identifier while a URL is a uniform resource locator. Hence every URL is a URI, abstractly speaking, but not every URI is a URL. This is because there is another subcategory of URIs, uniform resource names (URNs), which name resources but do not specify how to locate them. The mailto, news, and isbn URIs shown above are examples of URNs.

Which translated means: this Uri can represent almost anything.

Often this Uris represent a folder or a file but not always. And different Uris can point to the same file/folder

Permission over an Uri

To operate (read, delete, update, create) a file or folder within a directory, you need first to request permission of the user. These permissions are represented as UriPermission, reference.

API Labeling

See the label reference here.

API reference

Original API. These methods exists only in this package.

Because methods are an abstraction from native API, for example: openDocumentTree is an abstraction because there's no such method in native Android, there you need to create a intent and start an activity which is not the goal of this package (re-create all Android APIs) but provide a powerful fully-configurable API to call these APIs.

openDocumentTree

This API allows you grant Uris permission by calling like this:

final Uri? grantedUri = await openDocumentTree();

if (grantedUri != null) {
  print('Now I have permission over this Uri: $grantedUri');
}

openDocument

Same as openDocumentTree but for file URIs, you can request user to select a file and filter by:

  • Single or multiple files.
  • Mime type.

You can also specify if you want a one-time operation (persistablePermission = false) and if you don't need write access (grantWritePermission = false).

const kDownloadsFolder =
    'content://com.android.externalstorage.documents/tree/primary%3ADownloads/document/primary%3ADownloads';

final List<Uri>? selectedDocumentUris = await openDocument(
  // if you have a previously saved URI,
  // you can use the specify the tree you user will see at startup of the file picker.
  initialUri: Uri.parse(kDownloadsFolder),

  // whether or not allow the user select multiple files.
  multiple: true,

  // whether or not the selected URIs should be persisted across app and device reboots.
  persistablePermission: true,

  // whether or not grant write permission required to edit file metadata (name) and it's contents.
  grantWritePermission: true,

  // whether or not filter by mime type.
  mimeType: 'image/*' // default '*/*'
);

if (selectedDocumentUris == null) {
  return print('User cancelled the operation.');
}

// If [selectedDocumentUris] are [persistablePermission]s then it will be returned by this function
// along with any another URIs you've got permission over.
final List<UriPermission> persistedUris = await persistedUriPermissions();

listFiles

This method list files lazily over a granted uri:

Note DocumentFileColumn.id is optional. It is required to fetch the file list from native API. So it is enabled regardless if you include this column or not. And this applies only to this API (listFiles).

/// *Must* be a granted uri from `openDocumentTree`, or a URI representing a child under such a granted uri.
final Uri myGrantedUri = ...
final DocumentFile? documentFileOfMyGrantedUri = await myGrantedUri.toDocumentFile();

if (documentFileOfMyGrantedUri == null) {
  return print('This is not a valid Uri permission or you do not have the permission');
}

/// Columns/Fields you want access. Android handle storage as database.
/// Allow you specify only the fields you need to use, avoiding querying unnecessary data
const List<DocumentFileColumn> columns = <DocumentFileColumn>[
  DocumentFileColumn.displayName,
  DocumentFileColumn.size,
  DocumentFileColumn.lastModified,
  DocumentFileColumn.id, // Optional column, will be available/queried regardless if is or not included here
  DocumentFileColumn.mimeType,
];

final List<DocumentFile> files = [];

final Stream<DocumentFile> onNewFileLoaded = documentFileOfMyGrantedUri.listFiles(columns);

onNewFileLoaded.listen((file) => files.add(file), onDone: () => print('All files were loaded'));

openDocumentFile

Open a file uri in a external app, by starting a new activity with ACTION_VIEW Intent.

final Uri fileUri = ...

/// This call will prompt the user: "Open with" dialog
/// Or will open directly in the app if this there's only a single app that can handle this file type.
await openDocumentFile(fileUri);

getDocumentContent

Read a document file from its uri by opening a input stream and returning its bytes.

/// See also: [getDocumentContentAsString]
final Uri uri = ...

final Uint8List? fileContent = await getDocumentContent(uri);

/// Handle [fileContent]...

/// If the file is intended to be human readable, you can convert the output to [String]:
print(utf8.decode(fileContent));

getRealPathFromUri

Helper method to generate the file path of the given uri. This returns the real path to work with native old File API instead Uris, be aware this approach is no longer supported on Android 10+ (API 29+) and though new, this API is marked as deprecated and should be migrated to a scoped-storage approach.

See Get real path from URI, Android KitKat new storage access framework for details.

final Uri uri = ...;

final String? filePath = await getRealPathFromUri(myUri);

final File file = File(filePath);

Mirror methods

Mirror methods are available to provide an way to call a native method without using any abstraction, available mirror methods:

exists

Mirror of DocumentFile.exists

Returns true if a given uri exists.

final Uri uri = ...

if (await exists(uri) ?? false) {
  print('There is no granted Uris');
} else {
  print('My granted Uris: $grantedUris');
}

persistedUriPermissions

Mirror of ContentResolver.getPersistedUriPermissions

Basically this allow get the granted Uris permissions after the app restarts without the need of requesting the folders again.

final List<UriPermission>? grantedUris = await persistedUriPermissions();

if (grantedUris == null) {
  print('There is no granted Uris');
} else {
  print('My granted Uris: $grantedUris');
}

From the official docs:

Return list of all URI permission grants that have been persisted by the calling app. That is, the returned permissions have been granted to the calling app. Only persistable grants taken with takePersistableUriPermission(android.net.Uri, int) are returned. Note: Some of the returned URIs may not be usable until after the user is unlocked.

releasePersistableUriPermission

Mirror of ContentResolver.releasePersistableUriPermission

Opposite of openDocumentTree. This method revoke all permissions you have under a specific Uri. This should be used to allow the user revoke the permission of Uris inside your app without needing revoking at OS level.

final List<UriPermission> grantedUris = ...

/// Revoke all granted Uris
for (final UriPermission uri of grantedUris) {
  await releasePersistableUriPermission(uri);
}

/// You can also revoke a single Uri
await releasePersistableUriPermission(grantedUris[0]);

createFileAsBytes

Mirror of DocumentFile.createFile

Create a file using raw bytes Uint8List.

Given the parent uri, creates a new child document file that represents a single file given the displayName, mimeType and its content in bytes (file name, file type and file content in raw bytes, respectively).

final Uri parentUri = ...
final String fileContent = 'My File Content';

final DocumentFile? createdFile = createFileAsBytes(
  parentUri,
  mimeType: 'text/plain',
  displayName: 'Sample File Name',
  bytes: Uint8List.fromList(utf8.encode(fileContent)),
);

writeToFileAsBytes

Write to a file using raw bytes Uint8List.

Given the document uri, opens the file in the specified mode and writes the bytes to it.

mode represents the mode in which the file will be opened for writing. Use FileMode.write for truncating (overwrite) and FileMode.append for appending to the file.

final Uri documentUri = ...
final String fileContent = 'My File Content';

/// Write to a file using a [Uint8List] as file contents [bytes]
final bool? success = writeToFileAsBytes(
  documentUri,
  bytes: Uint8List.fromList(utf8.encode(fileContent)),
  mode: FileMode.write,
);

/// Append to a file using a [Uint8List] as file contents [bytes]
final bool? success = writeToFileAsBytes(
  documentUri,
  bytes: Uint8List.fromList(utf8.encode(fileContent)),
  mode: FileMode.write,
);

canRead

Mirror of DocumentFile.canRead

Returns true if the caller can read the given uri, that is, if has the properly permissions.

final Uri uri = ...

if (await canRead(uri) ?? false) {
  print('I have permissions to read $uri');

  final Uint8List? fileContent = await getDocumentContent(uri);

  /// ...
} else {
  final UriPermission? permission = openDocumentTree(uri);

  /// ...
}

canWrite

Mirror of DocumentFile.canWrite

Returns true if the caller can write the given uri, that is, if has the properly permissions.

final Uri uri = ...

if (await canWrite(uri) ?? false) {
  print('I have permissions to write $uri');

  final Uint8List? fileContent = await renameTo(uri, 'New File Name');

  /// ...
} else {
  final UriPermission? permission = openDocumentTree(
    uri,
    grantWritePermission: true,
  );

  /// ...
}

getDocumentThumbnail

Mirror of DocumentsContract.getDocumentThumbnail

Returns the image thumbnail of a given uri, if any (e.g documents that can show a preview, like images of gifs, null otherwise).

final Uint8List? imageBytes;
final DocumentFile file = ...

final Uri? rootUri = file.metadata?.rootUri;
final String? documentId = file.data?[DocumentFileColumn.id] as String?;

if (rootUri == null || documentId == null) return;

final DocumentBitmap? bitmap = await getDocumentThumbnail(
  rootUri: rootUri,
  documentId: documentId,
  width: _size.width,
  height: _size.height,
);

if (bitmap == null || !mounted) return;

setState(() => imageBytes = bitmap.bytes);

/// Later on...
@override
Widget build(BuildContext context) {
  if (imageBytes == null) return Loading('My cool loading spinner');

  return Image.memory(imageBytes);
}

DocumentFileColumn

Mirror of DocumentsContract.Document.<Column>

Use this class to refer to the SAF queryable columns in methods that requires granular/partial data fetch.

For instance, in listFiles a large set can be returned, and to improve performance you can provide only the columns you want access/read.

/// Columns/Fields you want access. Android handle storage as database.
/// Allow you specify only the fields you need to use, avoiding querying unnecessary data
const List<DocumentFileColumn> columns = <DocumentFileColumn>[
  DocumentFileColumn.displayName,
  DocumentFileColumn.size,
  DocumentFileColumn.lastModified,
  DocumentFileColumn.id,
  DocumentFileColumn.mimeType,
];

final Stream<DocumentFile> onNewFileLoaded = documentFileOfMyGrantedUri.listFiles(columns);

delete

Mirror of DocumentFile.delete

Self explanatory, but just in case: delete the target uri (document file).

final Uri uri = ...

await delete(uri);

createDirectory

Mirror of DocumentFile.createDirectory

Self explanatory, but just in case: creates a new child document file that represents a directory given the displayName (folder name).

final Uri parentUri = ...

await createDirectory(parentUri, 'My Folder Name');

documentLength

Mirror of DocumentFile.length

Returns the length of this file in bytes. Returns 0 if the file does not exist, or if the length is unknown.

final Uri uri = ...

final int? fileSize = await documentLength(uri);

lastModified

Mirror of DocumentFile.lastModified

Returns the time DateTime when this file was last modified. Returns null if the file does not exist, or if the modified time is unknown.

final Uri uri = ...

final int? fileSize = await documentLength(uri);

findFile

Mirror of DocumentFile.findFile

Search through listFiles() for the first document matching the given display name, this method has a really poor performance for large data sets.

final Uri directoryUri = ...

final DocumentFile? match = await findFile(directoryUri, 'Target File Name');

fromTreeUri

Mirror of DocumentFile.fromTreeUri

Create a DocumentFile representing the document tree rooted at the given Uri.

final Uri uri = ...

final DocumentFile? treeUri = fromTreeUri(uri);

renameTo

Mirror of DocumentFile.renameTo

Self explanatory, but just in case: rename the given document file given its uri and a new display name.

final Uri uri = ...

await renameTo(uri, 'New Document Name');

parentFile

Mirror of DocumentFile.parentFile

Returns the parent document file of a given document file (uri).

null if you do not have permission to see the parent folder.

final Uri uri = ...

final DocumentFile? parentUri = await parentFile(uri);

copy

Mirror of DocumentsContract.copyDocument

Copy the given uri to a new destinationUri.

final Uri uri = ...
final Uri destination = ...

final DocumentFile? copiedFile = await copy(uri, destination);

Alias methods

These APIs are only shortcuts/alias, that is, they do not call native code directly, these are just convenient methods.

isPersistedUri

Alias for persistedUriPermissions

Check if a given Uri is persisted/granted, that is, you have permission over it.

/// Can be any Uri
final Uri maybeGrantedUri = ...

final bool ensureThisIsGrantedUri = await isPersistedUri(maybeGrantedUri);

if (ensureThisIsGrantedUri) {
  print('I have permission over the Uri: $maybeGrantedUri');
}

getDocumentContentAsString

Alias for getDocumentContent

Read a document file from its uri by opening a input stream, reading its bytes and converting to String.

final Uri uri = ...

final String? fileContent = await getDocumentContentAsString(uri);

print(fileContent);

createFileAsString

Alias for createFileAsBytes

Convenient method to create a file using content as String instead Uint8List.

final Uri parentUri = ...
final String fileContent = 'My File Content';

final DocumentFile? createdFile = createFileAsString(
  parentUri,
  mimeType: 'text/plain',
  displayName: 'Sample File Name',
  content: fileContent,
);

writeToFileAsString

Alias for writeToFileAsBytes

Convenient method to write to a file using content as String instead Uint8List.

final Uri documentUri = ...
final String fileContent = 'My File Content';

/// Write to a file using a [Uint8List] as file contents [bytes]
final bool? success = writeToFileAsString(
  documentUri,
  content: fileContent,
  mode: FileMode.write,
);

/// Append to a file using a [Uint8List] as file contents [bytes]
final bool? success = writeToFileAsBytes(
  documentUri,
  content: fileContent,
  mode: FileMode.write,
);

createFile

Alias for createFileAsBytes and createFileAsString

Convenient method to create a file using content as String or bytes as Uint8List.

You should provide either content or bytes, if both bytes will be used.

final Uri parentUri = ...
final String fileContent = 'My File Content';

/// Create a file using a [String] as file contents [content]
final DocumentFile? createdFile = createFile(
  parentUri,
  mimeType: 'text/plain',
  displayName: 'Sample File Name',
  content: fileContent,
);

/// Create a file using a [Uint8List] as file contents [bytes]
final DocumentFile? createdFile = createFile(
  parentUri,
  mimeType: 'text/plain',
  displayName: 'Sample File Name',
  content: Uint8List.fromList(utf8.encode(fileContent)),
);

writeToFile

Alias for writeToFileAsBytes and writeToFileAsString

Convenient method to write to a file using content as String or bytes as Uint8List.

You should provide either content or bytes, if both bytes will be used.

mode represents the mode in which the file will be opened for writing. Use FileMode.write for truncating and FileMode.append for appending to the file.

final Uri documentUri = ...
final String fileContent = 'My File Content';

/// Write to a file using a [String] as file contents [content]
final bool? success = writeToFile(
  documentUri,
  content: fileContent,
  mode: FileMode.write,
);

/// Append to a file using a [String] as file contents [content]
final bool? success = writeToFile(
  documentUri,
  content: fileContent,
  mode: FileMode.append,
);

/// Write to a file using a [Uint8List] as file contents [bytes]
final bool? success = writeToFile(
  documentUri,
  content: Uint8List.fromList(utf8.encode(fileContent)),
  mode: FileMode.write,
);

/// Append to a file using a [Uint8List] as file contents [bytes]
final bool? success = writeToFile(
  documentUri,
  content: Uint8List.fromList(utf8.encode(fileContent)),
  mode: FileMode.append,
);

External APIs (deprecated)

These APIs are from external Android libraries.

Will be moved to another package soon.

child

Mirror of com.anggrayudi.storage.file.DocumentFile.child

Get the direct child of the given uri. Can be used to verify if a file already exists and check for conflicts.

final Uri parentUri = ...

final DocumentFile? childDocument = child(parentUri, 'Sample File Name');

if (childDocument != null) {
  /// This child exists...
} else {
  /// Doesn't exists...
}

Internal Types (Classes)

Internal type (class). Usually they are only to keep a safe typing and are not usually intended to be instantiated for the package user.

DocumentFile

This class represents but is not the mirror of the original DocumentFile.

This class is not intended to be instantiated, and it is only used for typing and convenient purposes.

QueryMetadata

This class wraps useful metadata of the source queries returned by the DocumentFile.

This class is not intended to be instantiated, and it is only used for typing and convenience purposes.

DocumentBitmap

This class represent the bitmap/image of a document.

Usually the thumbnail of the document.

Should be used to show a list/grid preview of a file list.

See also getDocumentThumbnail.

This class is not intended to be instantiated, and it is only used for typing and convenient purposes.

Extensions

These are most alias methods implemented through Dart extensions.

Uri.toDocumentFile on Uri

Alias for DocumentFile.fromTreeUri(this)

This method convert this uri to the respective DocumentFile (if exists, otherwise null).

final Uri uri = ...

final DocumentFile? documentFile = uri.toDocumentFile();

Uri.openDocumentFile on Uri

Alias for openDocumentFile(this)

This method open the current uri in a third-part application through ACTION_VIEW intent.

final Uri uri = ...

await uri.openDocumentFile();

Android Official Documentation

The Storage Access Framework official documentation is available here.

All the APIs listed in this plugin module are derivated from the official docs.