Internal vs External Storage
Storage Types
Android offers three primary file storage locations:
Internal Storage — Private to your app, automatically deleted when the app is uninstalled.
// Writing to internal storage
val file = File(context.filesDir, "config.json")
file.writeText("{\"theme\": \"dark\"}")
// Reading from internal storage
val content = file.readText()
No permissions needed. Files are in data/data/com.example.app/files/.
External Storage — Shared with other apps, requires permissions on older Android versions.
// Scoped storage (Android 10+)
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "report.pdf")
file.outputStream().use { os ->
pdfBytes.inputStream().use { it.copyTo(os) }
}
External files dir is in Android/data/com.example.app/files/ — still app-private on modern Android.
Cache Storage — Temporary files the system may delete.
val cacheFile = File(context.cacheDir, "temp_image.jpg")
// System deletes cache files when storage is low
Choosing the Right Storage
| Use Case | Storage Type |
|---|---|
| App settings, configs | Internal |
| User documents, exports | External files |
| Temporary downloads | Cache |
| Shared media (photos) | MediaStore / SAF |
| Large datasets | Internal or external |
File Paths
// Internal
context.filesDir // /data/data/pkg/files
context.cacheDir // /data/data/pkg/cache
context.getDir("data", 0) // /data/data/pkg/app_data
// External (app-private)
context.getExternalFilesDir(null) // Android/data/pkg/files
context.getExternalFilesDir(DIRECTORY_PICTURES) // Android/data/pkg/files/Pictures
context.externalCacheDir // Android/data/pkg/cache
File Safety
Always check if a file exists before reading, and wrap operations in try-catch:
fun readFileSafely(context: Context, filename: String): String? {
return try {
File(context.filesDir, filename).takeIf { it.exists() }?.readText()
} catch (e: IOException) {
Log.e("FileStorage", "Failed to read $filename", e)
null
}
}
Permissions and Scoped Storage
The Permission Landscape
Storage permissions have evolved significantly across Android versions:
Android 9 and below: READ_EXTERNAL_STORAGE and WRITE_EXTERNAL_STORAGE give broad access to all files.
Android 10 (Scoped Storage): Apps can only access their own files and media they created. requestLegacyExternalStorage = false by default.
Android 11+: MANAGE_EXTERNAL_STORAGE for file managers and backup apps only. MediaStore for shared media.
Android 13+: Granular media permissions replace READ_EXTERNAL_STORAGE.
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
Requesting Permissions at Runtime
class StorageActivity : AppCompatActivity() {
private val permissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val allGranted = permissions.values.all { it }
if (allGranted) loadMedia()
else showPermissionDeniedMessage()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
// Android 13+: request granular media permissions
permissionLauncher.launch(arrayOf(
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
))
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
// Android 6-12: request READ_EXTERNAL_STORAGE
permissionLauncher.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE))
} else {
loadMedia()
}
}
}
Writing to Shared Storage with MediaStore
suspend fun saveImageToGallery(context: Context, bitmap: Bitmap, filename: String) {
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, filename)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues
) ?: return
context.contentResolver.openOutputStream(uri)?.use { os ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
contentValues.clear()
contentValues.put(MediaStore.Images.Media.IS_PENDING, 0)
context.contentResolver.update(uri, contentValues, null, null)
}
}
IS_PENDING prevents other apps from seeing the file until it's fully written.
Safe File Operations and Best Practices
Kotlin Extension Functions
Wrap file operations in extension functions for consistency:
fun Context.safeWrite(filename: String, content: String): Boolean {
return try {
File(filesDir, filename).writeText(content)
true
} catch (e: IOException) {
Log.e("FileStorage", "Write failed: $filename", e)
false
}
}
fun Context.safeRead(filename: String): String? {
return try {
val file = File(filesDir, filename)
if (file.exists()) file.readText() else null
} catch (e: IOException) {
Log.e("FileStorage", "Read failed: $filename", e)
null
}
}
Atomic Writes
Write to a temp file first, then rename. This prevents corruption if the app crashes mid-write:
suspend fun atomicWrite(context: Context, filename: String, content: String) {
withContext(Dispatchers.IO) {
val target = File(context.filesDir, filename)
val temp = File(context.filesDir, "$filename.tmp")
temp.writeText(content)
temp.renameTo(target)
}
}
Streaming Large Files
Don't load entire files into memory. Use streams:
fun copyFile(source: File, dest: File, onProgress: ((Long) -> Unit)? = null) {
source.inputStream().use { input ->
dest.outputStream().use { output ->
val buffer = ByteArray(8192)
var bytesRead: Int
var totalBytes = 0L
while (input.read(buffer).also { bytesRead = it } != -1) {
output.write(buffer, 0, bytesRead)
totalBytes += bytesRead
onProgress?.invoke(totalBytes)
}
}
}
}
Cleanup
Delete files you no longer need. Use cacheDir for temporary data the system can also reclaim:
fun cleanupOldFiles(dir: File, maxAgeMs: Long) {
dir.listFiles()?.filter { file ->
System.currentTimeMillis() - file.lastModified() > maxAgeMs
}?.forEach { it.delete() }
}
Quiz
1. Which storage location requires no runtime permissions and is private to your app?
2. What does IS_PENDING do in MediaStore ContentValues on Android 10+?
3. Why use atomic writes (write-to-temp then rename) for critical data?
4. Which permission model replaced READ_EXTERNAL_STORAGE on Android 13?
Flashcards
Question
What is the difference between filesDir and getExternalFilesDir()?
Click to reveal answer
Answer
filesDir is internal storage (always private, no permissions). getExternalFilesDir() is app-private external storage (deleted on uninstall, no permissions needed on Android 10+).
Question
When should you use cacheDir instead of filesDir?
Click to reveal answer
Answer
For temporary files the system can reclaim when storage is low. Cache files may disappear without the app deleting them.
Question
How does Scoped Storage restrict file access on Android 10+?
Click to reveal answer
Answer
Apps can only access their own app-private files and media they created. Accessing shared files requires MediaStore or Storage Access Framework.
Question
What permission is needed to save files to the device gallery on Android 13?
Click to reveal answer
Answer
No permission needed to write via MediaStore. READ_MEDIA_IMAGES or READ_MEDIA_VIDEO are only needed to read other apps' media.
Revision Notes
Key Takeaways
- 1. Internal storage is always private and requires no permissions
- 2. Scoped Storage on Android 10+ restricts apps to their own files and media they created
- 3. Atomic writes prevent corruption when the app crashes mid-write
- 4. MediaStore is the correct way to share media files with other apps
Interview Tips
- • Know the storage permission model for each Android version — interviewers ask about Scoped Storage
- • Explain why you chose internal vs external storage for a given scenario
- • Discuss atomic writes as a pattern for data integrity
- • Be ready to describe the MediaStore insertion flow with IS_PENDING
Cheat Sheet
File Storage Cheat Sheet
Internal Storage:
context.filesDir— private, no permissionscontext.cacheDir— system-reclaimable temp files
External App-Private:
context.getExternalFilesDir()— private, no permissions on Android 10+- Deleted when app is uninstalled
Shared Storage:
- MediaStore API for photos, videos, audio
- SAF for user-selected files
Permissions by Android Version:
- Android 9-: READ/WRITE_EXTERNAL_STORAGE
- Android 10: Scoped Storage (no broad access)
- Android 11+: MANAGE_EXTERNAL_STORAGE for file managers only
- Android 13+: READ_MEDIA_IMAGES/VIDEO/AUDIO
Best Practices:
- Use atomic writes (temp file + rename)
- Stream large files, don't load into memory
- Clean up old files periodically