Created
March 12, 2025 12:54
-
-
Save decodeandroid/bc4322e688ce5b96f20e9baea0947136 to your computer and use it in GitHub Desktop.
Android Storage: Comprehensive Guide with Simplified Explanation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.Manifest | |
import android.content.Context | |
import android.os.Build | |
import android.widget.Toast | |
import androidx.activity.compose.rememberLauncherForActivityResult | |
import androidx.activity.result.contract.ActivityResultContracts | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.PaddingValues | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.items | |
import androidx.compose.material.icons.Icons | |
import androidx.compose.material.icons.filled.Add | |
import androidx.compose.material3.FloatingActionButton | |
import androidx.compose.material3.Icon | |
import androidx.compose.material3.Scaffold | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.platform.LocalContext | |
import java.io.File | |
@Composable | |
fun ExternalStorageScreen(padding: PaddingValues) { | |
val context = LocalContext.current | |
var files by remember { mutableStateOf(emptyList<File>()) } | |
var showDialog by remember { mutableStateOf(false) } | |
var selectedFile by remember { mutableStateOf<File?>(null) } | |
fun loadExternalFiles() { | |
context.createExtDir().listFiles()?.let { | |
files = it.toList() | |
} | |
} | |
// Permission handling | |
val permissionLauncher = rememberLauncherForActivityResult( | |
ActivityResultContracts.RequestPermission() | |
) { isGranted -> | |
if (isGranted) loadExternalFiles() else Toast.makeText(context, "Permission denied", Toast.LENGTH_SHORT).show() | |
} | |
LaunchedEffect(Unit) { | |
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { | |
permissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) | |
} | |
loadExternalFiles() | |
} | |
Scaffold( | |
floatingActionButton = { | |
FloatingActionButton(onClick = { showDialog = true }) { | |
Icon(Icons.Default.Add, "Add file") | |
} | |
} | |
) { innerPadding -> | |
Column(modifier = Modifier.padding(padding).padding(innerPadding)) { | |
LazyColumn { | |
items(files) { file -> | |
FileListItem( | |
file = file, | |
onDelete = { | |
if (file.delete()) loadExternalFiles() | |
}, | |
onEdit = { | |
selectedFile = file | |
showDialog = true | |
} | |
) | |
} | |
} | |
} | |
} | |
if (showDialog) { | |
FileDialog( | |
title = if (selectedFile == null) "Create File" else "Edit File", | |
initialName = selectedFile?.name?.replace(".txt", "") ?: "", | |
initialContent = selectedFile?.readText() ?: "", | |
onDismiss = { | |
showDialog = false | |
selectedFile = null | |
}, | |
onConfirm = { name, content -> | |
try { | |
val directory = context.createExtDir() | |
val fileName = name.replace(".txt", "").plus(".txt") | |
val file = File(directory, fileName) | |
file.writeText(content) | |
loadExternalFiles() | |
showDialog = false | |
selectedFile = null | |
} catch (e: Exception) { | |
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show() | |
} | |
} | |
) | |
} | |
} | |
fun Context.createExtDir(): File { | |
val directory = getExternalFilesDir(null) | |
val file = File(directory, DIRECTORY_NAME) | |
if (file.exists().not()) file.mkdir() | |
return file | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.content.Context | |
import android.widget.Toast | |
import androidx.compose.foundation.layout.Column | |
import androidx.compose.foundation.layout.PaddingValues | |
import androidx.compose.foundation.layout.Row | |
import androidx.compose.foundation.layout.Spacer | |
import androidx.compose.foundation.layout.fillMaxWidth | |
import androidx.compose.foundation.layout.height | |
import androidx.compose.foundation.layout.padding | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.foundation.lazy.items | |
import androidx.compose.material.icons.Icons | |
import androidx.compose.material.icons.filled.Add | |
import androidx.compose.material.icons.filled.Delete | |
import androidx.compose.material.icons.filled.Edit | |
import androidx.compose.material3.AlertDialog | |
import androidx.compose.material3.Button | |
import androidx.compose.material3.Card | |
import androidx.compose.material3.FloatingActionButton | |
import androidx.compose.material3.Icon | |
import androidx.compose.material3.IconButton | |
import androidx.compose.material3.Scaffold | |
import androidx.compose.material3.Text | |
import androidx.compose.material3.TextField | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.text.font.FontWeight | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import java.io.File | |
@Composable | |
fun InternalStorageScreen(padding: PaddingValues) { | |
val context = LocalContext.current | |
var showDialog by remember { mutableStateOf(false) } | |
var selectedFile by remember { mutableStateOf<File?>(null) } | |
var files by remember { mutableStateOf(context.createIntDirectory().listFiles()?.toList() ?: emptyList()) } | |
// Refresh file list | |
fun loadInternalFiles() { | |
files = context.createIntDirectory().listFiles()?.toList() ?: emptyList() | |
} | |
Scaffold( | |
floatingActionButton = { | |
FloatingActionButton(onClick = { showDialog = true }) { | |
Icon(Icons.Default.Add, "Add file") | |
} | |
} | |
) { innerPadding -> | |
Column(modifier = Modifier.padding(padding).padding(innerPadding)) { | |
LazyColumn { | |
items(files) { file -> | |
FileListItem( | |
file = file, | |
onDelete = { | |
if (file.delete()) loadInternalFiles() | |
}, | |
onEdit = { | |
selectedFile = file | |
showDialog = true | |
} | |
) | |
} | |
} | |
} | |
} | |
if (showDialog) { | |
FileDialog( | |
title = if (selectedFile == null) "Create File" else "Edit File", | |
initialName = selectedFile?.name?.replace(".txt", "") ?: "", | |
initialContent = selectedFile?.readText() ?: "", | |
onDismiss = { | |
showDialog = false | |
selectedFile = null | |
}, | |
onConfirm = { name, content -> | |
try { | |
//1. one way | |
/*context.openFileOutput(name, Context.MODE_PRIVATE).use { | |
it.write(content.toByteArray()) | |
}*/ | |
//2. second way | |
val directory = context.createIntDirectory() | |
val fileName = name.replace(".txt", "").plus(".txt") | |
val file = File(directory, fileName) | |
file.writeText(content) | |
loadInternalFiles() | |
showDialog = false | |
selectedFile = null | |
} catch (e: Exception) { | |
Toast.makeText(context, "Error: ${e.message}", Toast.LENGTH_SHORT).show() | |
} | |
} | |
) | |
} | |
} | |
const val DIRECTORY_NAME = "DecodeAndroid" | |
fun Context.createIntDirectory(): File { | |
val directory = filesDir | |
val file = File(directory, DIRECTORY_NAME) | |
if (!file.exists()) file.mkdir() | |
return file | |
} | |
@Composable | |
fun FileListItem(file: File, onDelete: () -> Unit, onEdit: () -> Unit) { | |
Card(modifier = Modifier.padding(8.dp)) { | |
Row( | |
modifier = Modifier | |
.fillMaxWidth() | |
.padding(10.dp), | |
verticalAlignment = Alignment.CenterVertically | |
) { | |
Column(modifier = Modifier.weight(1f)) { | |
Text(file.name.replace(".txt", ""), fontWeight = FontWeight.Bold, fontSize = 18.sp) | |
Text(file.readText(), fontWeight = FontWeight.Light, fontSize = 18.sp) | |
Text("${file.length()} bytes", fontWeight = FontWeight.Normal, fontSize = 18.sp) | |
} | |
IconButton(onClick = onEdit) { | |
Icon(Icons.Default.Edit, "Edit") | |
} | |
IconButton(onClick = onDelete) { | |
Icon(Icons.Default.Delete, "Delete") | |
} | |
} | |
} | |
} | |
// CommonDialog.kt | |
@Composable | |
fun FileDialog( | |
title: String, | |
initialName: String = "", | |
initialContent: String = "", | |
onDismiss: () -> Unit, | |
onConfirm: (name: String, content: String) -> Unit | |
) { | |
var fileName by remember { mutableStateOf(initialName) } | |
var content by remember { mutableStateOf(initialContent) } | |
AlertDialog( | |
onDismissRequest = onDismiss, | |
title = { Text(title) }, | |
text = { | |
Column { | |
TextField( | |
value = fileName, | |
onValueChange = { fileName = it }, | |
label = { Text("File name") } | |
) | |
Spacer(modifier = Modifier.height(8.dp)) | |
TextField( | |
value = content, | |
onValueChange = { content = it }, | |
label = { Text("Content") }, | |
modifier = Modifier.height(150.dp) | |
) | |
} | |
}, | |
confirmButton = { | |
Button(onClick = { onConfirm(fileName, content) }) { | |
Text("Save") | |
} | |
}, | |
dismissButton = { | |
Button(onClick = onDismiss) { Text("Cancel") } | |
} | |
) | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
setContent { | |
val tabs = listOf("Internal", "External/Scoped", "Shared") | |
var selectedTab by remember { mutableStateOf(0) } | |
Scaffold( | |
topBar = { | |
TabRow(selectedTabIndex = selectedTab) { | |
tabs.forEachIndexed { index, title -> | |
Tab( | |
selected = selectedTab == index, | |
onClick = { selectedTab = index }, | |
text = { Text(title) } | |
) | |
} | |
} | |
} | |
) { padding -> | |
when (selectedTab) { | |
0 -> InternalStorageScreen(padding) | |
1 -> ExternalStorageScreen(padding) | |
2 -> SharedStorageScreen(padding) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" | |
android:maxSdkVersion="28" /> | |
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" | |
android:maxSdkVersion="28" /> | |
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" | |
tools:ignore="ScopedStorage" /> <!-- For Android 11+ --> | |
<!-- For Android 13+ media permissions --> | |
<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" /> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment