Как быстро работать с файлами и I/O в Android
Коротко: для приватных данных используйте app-specific (getFilesDir/getExternalFilesDir), для медиа и загрузок — MediaStore, а для доступа к произвольным папкам (включая SD) — Storage Access Framework (SAF) с takePersistableUriPermission; MANAGE_EXTERNAL_STORAGE — крайний вариант и редко одобряется.
Типы хранилищ и что выбирать
- App-specific (getFilesDir(), getCacheDir(), getExternalFilesDir()):
- Нет runtime‑разрешений, данные удаляются при удалении приложения. Используйте для конфиденциальных и временных данных.
- Пример записи в внутреннее хранилище:
val file = File(filesDir, "notes.txt")
file.writeText("Hello Android")
- Публичное external (Downloads, Pictures и т.п.):
- На Android 11+ прямой доступ ограничен. Для файлов типа загрузок лучше MediaStore.
- Пример записи в Downloads через MediaStore:
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "file.txt")
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val uri = contentResolver.insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values)
contentResolver.openOutputStream(uri!!)?.use { it.write("data".toByteArray()) }
На Android 11+ старые файлы в DIRECTORY_DOWNLOADS могут быть недоступны напрямую — для работы с существующими файлами используйте SAF.
- MediaStore:
- Подходит для фото/видео/аудио, можно запрашивать READ/WRITE для старых API.
- MANAGE_EXTERNAL_STORAGE:
- Даёт полный доступ, но Google Play ограничивает его применение. Используйте только если это действительно нужно (файловые менеджеры).
SAF: практика и примеры
SAF — Intent‑базированный способ получить разрешение пользователя на папку/файл. Поддерживает SD‑карты и даёт персистентный доступ.
Пример открытия дерева и сохранения пермишнов:
private fun openFolderPicker() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(intent, PICKER_CODE)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == PICKER_CODE && resultCode == Activity.RESULT_OK) {
data?.data?.let { uri ->
contentResolver.takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
val tree = DocumentFile.fromTreeUri(this, uri)
val newFile = tree?.createFile("text/plain", "notes.txt")
contentResolver.openOutputStream(newFile!!.uri)?.use { out ->
out.write("Content".toByteArray())
}
}
}
}
Преимущества: персистентный доступ, работа с SD-картами, обход Scoped Storage при явном согласии пользователя.
Не забудьте takePersistableUriPermission — без него доступ пропадёт после перезагрузки устройства.
Миграция, ограничения по версиям и best practices
- До Android 9 (API <= 28) — прямой доступ был проще.
- Android 10 (API 29) — появилась опция requestLegacyExternalStorage=true.
- Android 11+ — Scoped Storage обязателен; используйте MediaStore и SAF.
- Android 14+ — Photos Picker для доступа к галерее без разрешений.
Рекомендации:
- Мигрируйте код сразу на MediaStore + SAF.
- Для targetSdk >= 33/34 тестируйте на эмуляторах с разными API.
- Обрабатывайте SecurityException и ошибки ввода‑вывода; всегда проверяйте null‑uri.
- Для bulk‑операций (копирование папки) итерируйте по DocumentFile, создавая файлы по одному.
Частые ошибки
- Не вызывать takePersistableUriPermission — потеря доступа после рестарта.
- Игнорирование runtime‑permissions на старых API (READ_EXTERNAL_STORAGE).
- Ожидание, что Environment.getExternalStoragePublicDirectory() будет работать одинаково на всех API.
- Попытка массовых операций без учёта ограничений Scoped Storage.
FAQ
Q: Нужно ли запрашивать MANAGE_EXTERNAL_STORAGE? A: Только если вашему приложению нужен полный доступ ко всем файлам (файловые менеджеры). Скорее всего хватит SAF + MediaStore.
Q: Как работать с SD‑картой? A: Через SAF: пользователь выбирает корень/папку SD, вы сохраняете persistable permission и работаете через DocumentFile.
Q: Как сохранить приватные данные? A: В app-specific storage (внутреннее или внешнее через getExternalFilesDir) — без разрешений и безопасно.
Q: Что делать с legacy‑кодом, который использует direct file paths? A: Переписать на MediaStore/SAF или ограничить работу локально в app-specific. Если временно нужен доступ, requestLegacyExternalStorage работает только до API 30.