Как быстро работать с файлами и 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.