---
title: "Storage Access Framework e DocumentFile no Android com Kotlin em 2026"
url: "https://kotlin.dev.br/blog/storage-access-framework-documentfile-android-kotlin-2026/"
markdown_url: "https://kotlin.dev.br/blog/storage-access-framework-documentfile-android-kotlin-2026.MD"
description: "Use Storage Access Framework, OpenDocument e DocumentFile com Kotlin e Compose para abrir, criar e persistir arquivos sem permissão ampla de armazenamento."
date: "2026-09-16"
author: "Karina Melo"
---

# Storage Access Framework e DocumentFile no Android com Kotlin em 2026

Use Storage Access Framework, OpenDocument e DocumentFile com Kotlin e Compose para abrir, criar e persistir arquivos sem permissão ampla de armazenamento.


**Resposta rápida:** para abrir PDF, planilha, ZIP ou qualquer documento no Android moderno sem pedir acesso amplo ao armazenamento, use o **Storage Access Framework (SAF)** com `ActivityResultContracts.OpenDocument`, `CreateDocument` ou `OpenDocumentTree`. Em Jetpack Compose, registre o contrato com `rememberLauncherForActivityResult`, receba um `Uri` e leia/escreva via `ContentResolver`. Quando precisar navegar em uma árvore (pasta escolhida pelo usuário), use `DocumentFile.fromTreeUri`. Persista a concessão com `contentResolver.takePersistableUriPermission` se o acesso precisa sobreviver a reinícios. SAF não substitui [Photo Picker](/blog/photo-picker-android-kotlin-compose-2026/) para fotos/vídeos pontuais nem MediaStore para galeria própria — ele é a API certa para **documentos e pastas escolhidos pelo usuário**.

Em apps brasileiros de campo, financeiro, RH e logística, o padrão “anexar comprovante”, “exportar relatório” ou “escolher pasta de backup” ainda aparece com `READ_EXTERNAL_STORAGE`, caminhos `/sdcard/...` e `File` direto. Em 2026 isso quebra em aparelhos com *scoped storage*, gera rejeição de privacidade e falha em provedores de nuvem (Drive, OneDrive, Downloads do sistema). Este guia fecha a lacuna: quando usar SAF, como implementar com Kotlin e Compose, como persistir permissões e como evitar os erros que mais aparecem em code review.

## Quando usar SAF (e quando não usar)

Faça a pergunta: **o usuário escolhe explicitamente o arquivo ou a pasta?**

| Necessidade | API recomendada | Permissão ampla? |
|---|---|---|
| Anexar PDF, XML, CSV, ZIP, DOCX | `OpenDocument` | Não |
| Exportar/salvar um arquivo gerado pelo app | `CreateDocument` | Não |
| Backup/export contínuo em pasta escolhida | `OpenDocumentTree` + `DocumentFile` | Não |
| Escolher foto/vídeo pontual | [Photo Picker](/blog/photo-picker-android-kotlin-compose-2026/) | Não |
| Galeria própria listando a biblioteca | MediaStore | Pode exigir |
| Arquivos privados só do app | `filesDir` / `cacheDir` | Não |
| Download grande gerenciado pelo sistema | `DownloadManager` | Depende do destino |

A regra prática: **se o usuário aponta o documento, use SAF**. Se o app precisa varrer o armazenamento inteiro “porque talvez tenha um PDF”, redesenhe o fluxo. Pedir acesso amplo para anexar um comprovante é o mesmo antipadrão de pedir `READ_MEDIA_IMAGES` para avatar.

SAF também é o caminho correto quando o conteúdo pode estar em provedores remotos. O `Uri` devolvido pode apontar para nuvem; o app não deve assumir caminho local nem `File.exists()`.

## Dependências e contratos

Os contratos principais estão em AndroidX Activity. No Version Catalog:

```toml
# gradle/libs.versions.toml
[versions]
activity = "<versao-estavel-atual>"
documentfile = "<versao-estavel-atual>"

[libraries]
androidx-activity-compose = {
  module = "androidx.activity:activity-compose",
  version.ref = "activity"
}
androidx-documentfile = {
  module = "androidx.documentfile:documentfile",
  version.ref = "documentfile"
}
```

```kotlin
// build.gradle.kts do módulo app
dependencies {
    implementation(libs.androidx.activity.compose)
    implementation(libs.androidx.documentfile)
}
```

`documentfile` é opcional para abrir/criar um único arquivo via `ContentResolver`, mas vira o caminho mais legível ao trabalhar com pastas (`OpenDocumentTree`), listar filhos e criar subdocumentos.

Para o fluxo comum de anexar ou exportar, **não declare** `READ_EXTERNAL_STORAGE` / `MANAGE_EXTERNAL_STORAGE`. Se o app também tem galeria própria ou câmera, isole essas permissões nos fluxos que realmente precisam delas — veja o guia de [permissões no Android](/blog/permissoes-android-kotlin-2026/).

## Abrir um documento no Jetpack Compose

`OpenDocument` devolve um `Uri?`. `null` significa cancelamento:

```kotlin
@Composable
fun AnexarDocumentoButton(
    onDocumentoSelecionado: (Uri) -> Unit,
) {
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.OpenDocument(),
    ) { uri: Uri? ->
        uri?.let(onDocumentoSelecionado)
    }

    Button(
        onClick = {
            launcher.launch(
                arrayOf(
                    "application/pdf",
                    "image/*",
                    "text/csv",
                    "application/zip",
                )
            )
        }
    ) {
        Text("Anexar documento")
    }
}
```

Pontos que evitam regressão:

1. registre o launcher durante a composição, nunca dentro do `onClick`;
2. filtre por MIME reais do produto — `*/*` só quando fizer sentido;
3. valide de novo no app: o seletor ajuda, mas não é barreira de segurança;
4. trate cancelamento como fluxo normal, não como erro.

Para múltiplos arquivos:

```kotlin
val multiLauncher = rememberLauncherForActivityResult(
    contract = ActivityResultContracts.OpenMultipleDocuments(),
) { uris: List<Uri> ->
    if (uris.isNotEmpty()) onDocumentosSelecionados(uris)
}
```

Limite quantidade no domínio (ViewModel/caso de uso), não só na UI. Um usuário pode selecionar dezenas de PDFs grandes em rede móvel.

## Criar / exportar um arquivo

`CreateDocument` pede ao usuário onde salvar e com qual nome sugerido:

```kotlin
@Composable
fun ExportarRelatorioButton(
    sugerirNome: String = "relatorio-mensal.pdf",
    onArquivoCriado: (Uri) -> Unit,
) {
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.CreateDocument("application/pdf"),
    ) { uri ->
        uri?.let(onArquivoCriado)
    }

    OutlinedButton(onClick = { launcher.launch(sugerirNome) }) {
        Text("Exportar PDF")
    }
}
```

Depois escreva no `Uri` com `ContentResolver`:

```kotlin
fun escreverPdf(
    context: Context,
    destino: Uri,
    bytes: ByteArray,
) {
    context.contentResolver.openOutputStream(destino, "wt")?.use { output ->
        output.write(bytes)
        output.flush()
    } ?: error("Não foi possível escrever no destino escolhido")
}
```

Para arquivos grandes, evite `ByteArray` inteiro: gere em streaming (Okio, `BufferedOutputStream`, ou pipeline do gerador de PDF) e copie em blocos. O mesmo cuidado de memória do Photo Picker se aplica aqui.

## Nunca transforme o Uri em caminho físico

O erro clássico — `getRealPathFromUri()` — falha com SAF com ainda mais frequência do que com mídia. O `Uri` pode ser:

- `content://com.android.providers.downloads.documents/...`
- `content://com.google.android.apps.docs.storage/...`
- `content://com.android.externalstorage.documents/tree/...`

Abra sempre via `ContentResolver`:

```kotlin
fun lerBytes(context: Context, uri: Uri): ByteArray {
    return context.contentResolver
        .openInputStream(uri)
        ?.use { it.readBytes() }
        ?: error("Conteúdo indisponível")
}

fun copiarParaPrivado(
    context: Context,
    uri: Uri,
    destino: File,
) {
    context.contentResolver.openInputStream(uri).use { input ->
        requireNotNull(input) { "Não foi possível abrir o documento" }
        destino.outputStream().use { output ->
            input.copyTo(output, bufferSize = 8 * 1024)
        }
    }
}
```

Para upload posterior, fila offline ou parsing demorado, **copie para `filesDir`/`cacheDir`**. Guardar só `uri.toString()` e esperar que funcione semanas depois é frágil — especialmente sem permissão persistente.

## Metadados: nome, MIME e tamanho

Use `OpenableColumns` e `getType`:

```kotlin
data class DocumentoMeta(
    val displayName: String?,
    val mimeType: String?,
    val sizeBytes: Long?,
)

fun consultarMeta(context: Context, uri: Uri): DocumentoMeta {
    val resolver = context.contentResolver
    val mimeType = resolver.getType(uri)
    val projection = arrayOf(
        OpenableColumns.DISPLAY_NAME,
        OpenableColumns.SIZE,
    )

    resolver.query(uri, projection, null, null, null)?.use { cursor ->
        if (cursor.moveToFirst()) {
            val nameIdx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
            val sizeIdx = cursor.getColumnIndex(OpenableColumns.SIZE)
            return DocumentoMeta(
                displayName = nameIdx.takeIf { it >= 0 }?.let(cursor::getString),
                mimeType = mimeType,
                sizeBytes = sizeIdx.takeIf { it >= 0 && !cursor.isNull(it) }
                    ?.let(cursor::getLong),
            )
        }
    }

    return DocumentoMeta(displayName = null, mimeType = mimeType, sizeBytes = null)
}
```

Nem todo provedor informa tamanho. Quando vier nulo, conte bytes durante a cópia e aborte ao ultrapassar o limite do produto. No backend, revalide MIME real, magic bytes e tamanho — nome e `Content-Type` do cliente não são prova.

## Persistindo acesso entre sessões

Há três estratégias, em ordem de previsibilidade:

1. **Consumir imediatamente** — ler/enviar enquanto o fluxo está ativo.
2. **Copiar para armazenamento privado** — melhor para upload, OCR, fila [offline-first](/blog/android-offline-first-kotlin-2026/).
3. **Permissão persistente de URI** — quando o app precisa reabrir o mesmo documento/pasta depois.

```kotlin
fun persistirLeitura(context: Context, uri: Uri) {
    val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
    context.contentResolver.takePersistableUriPermission(uri, flags)
}

fun persistirLeituraEscrita(context: Context, uri: Uri) {
    val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    context.contentResolver.takePersistableUriPermission(uri, flags)
}
```

Cuidados importantes:

- só funciona se o contrato/provedor concedeu flag persistível (`OpenDocument` / `OpenDocumentTree` normalmente permitem; não assuma o mesmo de todo seletor);
- liberte com `releasePersistableUriPermission` quando o usuário remover o anexo ou trocar a pasta;
- persista o `Uri` (string) **e** um identificador interno do negócio; não use o URI como chave primária exclusiva sem plano B;
- teste reinício do aparelho e revogação pelo usuário em Ajustes.

Liste concessões ativas quando precisar auditar:

```kotlin
val persistidos = context.contentResolver.persistedUriPermissions
```

## OpenDocumentTree + DocumentFile

Quando o produto pede “escolha a pasta de exportação” ou “sincronize com esta pasta”, use árvore:

```kotlin
@Composable
fun EscolherPastaBackup(
    onPastaSelecionada: (Uri) -> Unit,
) {
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.OpenDocumentTree(),
    ) { treeUri ->
        if (treeUri != null) {
            // Persistir leitura+escrita antes de usar depois.
            onPastaSelecionada(treeUri)
        }
    }

    Button(onClick = { launcher.launch(null) }) {
        Text("Escolher pasta de backup")
    }
}
```

Com `DocumentFile`:

```kotlin
fun criarBackupJson(
    context: Context,
    treeUri: Uri,
    nomeArquivo: String,
    conteudo: String,
): Uri {
    val root = DocumentFile.fromTreeUri(context, treeUri)
        ?: error("Árvore inválida")

    val existente = root.findFile(nomeArquivo)
    val alvo = existente
        ?: root.createFile("application/json", nomeArquivo)
        ?: error("Não foi possível criar $nomeArquivo")

    context.contentResolver.openOutputStream(alvo.uri, "wt")?.use { output ->
        output.write(conteudo.toByteArray(Charsets.UTF_8))
    } ?: error("Falha ao escrever backup")

    return alvo.uri
}

fun listarFilhos(
    context: Context,
    treeUri: Uri,
): List<DocumentFile> {
    val root = DocumentFile.fromTreeUri(context, treeUri) ?: return emptyList()
    return root.listFiles().toList()
}
```

Boas práticas com árvore:

- não percorra recursivamente pastas enormes na main thread;
- filtre por `isFile` / `type` / nome antes de abrir;
- trate `createFile` retornando `null` (provedor sem suporte a escrita, pasta só leitura, quota);
- evite assumir que `findFile` é case-sensitive igual em todos os provedores;
- para sync periódico, combine a pasta persistida com [WorkManager](/blog/workmanager-kotlin-android-2026/) — não mantenha um foreground service só para “vigiar a pasta”.

## ViewModel: UI abre, domínio processa

O launcher fica na UI. Validação, cópia e enfileiramento ficam no domínio:

```kotlin
class AnexoViewModel(
    private val importador: DocumentoImportador,
) : ViewModel() {

    private val _state = MutableStateFlow(AnexoState())
    val state = _state.asStateFlow()

    fun onDocumentoEscolhido(uri: Uri) {
        viewModelScope.launch {
            _state.update { it.copy(importando = true, erro = null) }

            runCatching { importador.importar(uri) }
                .onSuccess { local ->
                    _state.update {
                        it.copy(
                            importando = false,
                            arquivoLocalId = local.id,
                            nomeExibicao = local.nome,
                        )
                    }
                }
                .onFailure { e ->
                    _state.update {
                        it.copy(
                            importando = false,
                            erro = e.message ?: "Não foi possível importar o documento",
                        )
                    }
                }
        }
    }
}
```

Encapsule `ContentResolver` atrás de `DocumentoImportador`. Isso permite testar limites de MIME/tamanho sem abrir o seletor do sistema e evita `Context` genérico vazando para a camada de apresentação.

## Upload e processamento em background

Fluxo confiável para comprovante/anexo:

1. usuário escolhe o documento no SAF;
2. app valida MIME/tamanho;
3. copia para área privada com nome interno aleatório;
4. persiste item `PENDING` no banco;
5. WorkManager envia com constraint de rede;
6. backend revalida e devolve ID final;
7. app marca `UPLOADED` e limpa temporário.

Passe só o ID no `Data` do worker — nunca o arquivo inteiro nem um URI sem garantia de acesso. O padrão é o mesmo do guia de [WorkManager](/blog/workmanager-kotlin-android-2026/) e do fluxo de mídia do Photo Picker.

## SAF vs Photo Picker vs armazenamento privado

| Pergunta | Resposta curta |
|---|---|
| É foto/vídeo de anexar agora? | Photo Picker |
| É PDF/planilha/ZIP/documento genérico? | `OpenDocument` |
| Preciso salvar um arquivo gerado? | `CreateDocument` |
| Preciso de pasta duradoura? | `OpenDocumentTree` + permissão persistente |
| O arquivo só existe para o app? | `filesDir` / DataStore / Room |
| Preciso listar toda a galeria? | MediaStore + permissões |

Não misture as APIs sem necessidade. Um botão “Anexar” que às vezes abre galeria com permissão ampla e às vezes SAF confunde o usuário e complica testes. Separe “Anexar foto” e “Anexar documento” quando o produto tiver os dois.

Para dados sensíveis (comprovantes, contratos, documentos de identidade), combine SAF com as práticas de [segurança de dados locais](/blog/seguranca-dados-locais-android-kotlin-2026/) e o recorte de [LGPD no Android](/blog/lgpd-android-kotlin-2026/): minimize retenção, criptografe em repouso quando o risco pedir, e não logue URI/nome completo em analytics.

## Testes

### Unitário

- MIME permitido/proibido;
- limite de bytes com tamanho conhecido e desconhecido;
- cópia interrompida no meio;
- limpeza após sucesso/cancelamento;
- idempotência do import (mesmo URI duas vezes não duplica negócio).

### UI / instrumentado

- botão dispara o launcher;
- retorno com URI atualiza estado;
- cancelamento preserva estado anterior;
- rotação não importa duas vezes;
- processo recriado recupera item já copiado;
- árvore persistida continua acessível após cold start (teste dedicado, poucos casos).

Não dependa de strings do seletor do fabricante. Injete um callback/`DocumentoImportador` fake na maior parte da suíte e reserve poucos testes end-to-end reais para o SAF.

## Erros comuns

1. **Pedir armazenamento amplo para anexar um PDF.** SAF resolve sem isso.
2. **Converter URI em path de `File`.** Use `ContentResolver` / `DocumentFile`.
3. **Esquecer permissão persistente** e achar que o URI “sempre funciona”.
4. **Persistir e nunca liberar** concessões órfãs.
5. **Carregar PDF de 80 MB em `ByteArray`.** Faça streaming.
6. **Confiar na extensão `.pdf`.** Consulte MIME e valide conteúdo.
7. **Criar launcher no clique.** Registre na composição.
8. **Usar `OpenDocumentTree` para um único arquivo.** É UX e permissão em excesso.
9. **Assumir escrita em qualquer árvore.** Provedores podem ser só leitura.
10. **Logar URI/nome do documento.** Pode conter dados pessoais.
11. **Fazer listagem recursiva pesada na main thread.**
12. **Misturar Photo Picker e SAF no mesmo botão sem deixar a escolha clara.**

## Checklist de produção

- `activity-compose` (e `documentfile` se usar árvore) no Version Catalog.
- `OpenDocument` / `CreateDocument` / `OpenDocumentTree` conforme o caso.
- Sem permissão ampla de armazenamento no fluxo de anexo/export.
- MIME filters alinhados ao produto + revalidação local.
- `Uri` tratado com `ContentResolver`, sem “real path”.
- Cópia privada antes de upload/OCR adiado.
- `takePersistableUriPermission` só quando o acesso precisa sobreviver; `release` no ciclo de vida do vínculo.
- WorkManager com ID persistido e constraint de rede.
- Backend revalida tipo/tamanho/conteúdo.
- Temporários com política de limpeza.
- Cancelamento, cold start e provedor de nuvem testados.
- Acessibilidade: botões com rótulo claro (“Anexar PDF”, “Escolher pasta”).

## Perguntas frequentes

### Storage Access Framework precisa de permissão de armazenamento?

Para seleção explícita de arquivo/pasta, não. O sistema concede acesso aos itens (ou à árvore) escolhidos pelo usuário. Permissões amplas entram em outros desenhos de produto, não neste fluxo.

### Qual a diferença entre OpenDocument e Photo Picker?

Photo Picker é otimizado para imagens/vídeos com UX de galeria moderna e privacidade por item. `OpenDocument` cobre documentos genéricos (PDF, CSV, ZIP, etc.) e tipos MIME arbitrários. Use cada um no seu botão.

### Como manter acesso à pasta depois de fechar o app?

Com `OpenDocumentTree`, chame `takePersistableUriPermission` com flags de leitura/escrita, salve o `Uri` da árvore e reabra com `DocumentFile.fromTreeUri`. Ofereça na UI uma ação para trocar ou revogar a pasta.

### DocumentFile ainda vale a pena em 2026?

Sim, como camada conveniente sobre URIs de documento/árvore. Para um único arquivo aberto/criado, `ContentResolver` puro basta. Para criar filhos, listar e navegar pasta, `DocumentFile` reduz boilerplate — sem eliminar a necessidade de validar retornos nulos e falhas do provedor.

### Posso usar SAF para backup automático noturno?

Sim: o usuário escolhe a pasta uma vez, você persiste a permissão e agenda o trabalho no WorkManager. Não mantenha serviço contínuo só para isso. Se o horário precisar ser exato (alarme), aí entra `AlarmManager` — outro tópico; backup de arquivo raramente precisa de alarme exato.

### O que fazer se openInputStream retornar null?

Mostre erro recuperável, ofereça escolher de novo e registre telemetria agregada (tipo de falha), sem URI completo. Causas comuns: permissão revogada, item apagado, provedor offline, árvore inválida.

## Conclusão

O **Storage Access Framework com Kotlin** é o padrão certo para documentos e pastas escolhidos pelo usuário no Android de 2026: menos permissão, melhor compatibilidade com nuvem e Downloads, e um contrato claro via `Uri`. A implementação enxuta — `OpenDocument`/`CreateDocument`/`OpenDocumentTree` + `ContentResolver`/`DocumentFile` — só se torna robusta quando você valida MIME/tamanho, evita caminhos físicos, decide entre cópia privada e permissão persistente, e processa em background com WorkManager.

A decisão de produto mais importante é separar **seleção pontual** de **acesso amplo**. Se o usuário aponta o arquivo, SAF (ou Photo Picker para mídia) resolve. Se o app insiste em varrer o aparelho, o custo aparece em privacidade, Play policy e suporte.

Para continuar, leia [Photo Picker com Compose](/blog/photo-picker-android-kotlin-compose-2026/), [permissões no Android](/blog/permissoes-android-kotlin-2026/), [segurança de dados locais](/blog/seguranca-dados-locais-android-kotlin-2026/), [WorkManager](/blog/workmanager-kotlin-android-2026/) e [offline-first](/blog/android-offline-first-kotlin-2026/). Para aplicar a stack em produtos reais, acompanhe as [vagas Kotlin e Android](/vagas/) no Kotlin Brasil.
