Skip to main content

04 - Persistent storage

Android filesystem

Android uses a file system that's similar to disk-based file systems on other platforms. In the Android file system, there are usually six main partitions found on every device. Some devices may come with a couple of additional partitions, which differ from model to model, but six primary partitions are found on every Android device.

PartitionDescriptionCan apps save files here?
/bootContains the kernel and ramdisk, essential for booting the device.No
/systemHouses the Android OS and pre-installed system apps; critical for the device's operation.No
/recoveryAllows booting into recovery mode for backups, factory resets, and maintenance tasks.No
/dataStores user data, including apps, settings, contacts, and messages.Yes (App Sandbox - This is the primary location for app-specific data. Each app has a private directory here (/data/data/<package_name>)).
/cacheHolds frequently accessed app data to improve performance and free up space.Yes (Limited)
/miscStores miscellaneous system settings like USB configuration and carrier ID.No

Other common partions include :

PartitionDescriptionCan apps save files here?
/sdcardUser-accessible storage for files and data; can refer to internal or external SD cards.Yes (Scoped Access)
/sd-extAn additional SD card partition for storing app data, often used with custom ROMs or mods.Yes (with Custom ROMs)

Types of application data storage

Android provides several ways to store data, each suitable for different purposes.

  • App-specific storage: Store files that are meant for your app's use only, either in dedicated directories within an internal storage volume or different dedicated directories within external storage. Use the directories within internal storage to save sensitive information that other apps shouldn't access.
  • Shared storage: Store files that your app intends to share with other apps, including media, documents, and other files.
  • Preferences: Store private, primitive data in key-value pairs.
  • Databases: Store structured data in a private database using the Room persistence library.
More about data/files types
Type of contentAccess methodPermissions neededCan other apps access?Files removed on app uninstall?
App-specific filesFiles meant for your app's use onlyFrom internal storage, getFilesDir() or getCacheDir() From external storage, getExternalFilesDir() or getExternalCacheDir()Never needed for internal storage Not needed for external storage when your app is used on devices that run Android 4.4 (API level 19) or higherNoYes
MediaShareable media files (images, audio files, videos)MediaStore APIREAD_EXTERNAL_STORAGE when accessing other apps' files on Android 11 (API level 30) or higher READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE when accessing other apps' files on Android 10 (API level 29) Permissions are required for all files on Android 9 (API level 28) or lowerYes, though the other app needs the READ_EXTERNAL_STORAGE permissionNo
Documents and other filesOther types of shareable content, including downloaded filesStorage Access FrameworkNoneYes, through the system file pickerNo
App preferencesKey-value pairsJetpack Preferences libraryNoneNoYes
DatabaseStructured dataRoom persistence libraryNoneNoYes

App-Specific Storage

App-specific storage refers to files stored in directories dedicated to your app, either in internal storage (private to your app) or external storage (visible but scoped to your app starting from Android 10).

The use and useLines constructs are Kotlin features that ensure resources such as streams or buffers are closed properly after usage, avoiding memory leaks. For reading operations, a buffered reader is employed, which wraps an input stream to improve performance by reading chunks of data into memory instead of processing it byte by byte.

  • Internal Storage: Use this for sensitive information, as only your app can access these files.

    • Writing to internal storage
    fun writeToInternalStorage(filename: String, content: String) {
    openFileOutput(filename, Context.MODE_PRIVATE).use {
    it.write(content.toByteArray())
    }
    }
    • Reading from internal storage
    fun readFromInternalStorage(filename: String): String {
    return openFileInput(filename).bufferedReader().useLines { it.joinToString() }
    }
  • External Storage: Suitable for non-sensitive files. A File object encapsulates the file path and is combined with FileOutputStream to perform write operations, while FileInputStream with a buffered reader is used to read data in a structured way.

    • Writing to external storage
    fun writeToExternalStorage(filename: String, content: String) {
    if (Environment.MEDIA_MOUNTED == Environment.getExternalStorageState()) {
    val file = File(getExternalFilesDir(null), filename)
    FileOutputStream(file).use {
    it.write(content.toByteArray())
    }
    }
    }
    • Reading from external storage
    private fun readFromExternalStorage(filename: String): String {
    val file = File(getExternalFilesDir(null), filename)
    return if (file.exists()) {
    FileInputStream(file).bufferedReader().useLines { it.joinToString() }
    } else {
    "File not found"
    }
    }
    info

    Additional permission have to be specified in the AndroidManifest.xml file.

     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

Shared Storage

Shared storage is for files like media (images, videos, audio) and documents that can be accessed by other apps. MediaStore API is used to store and retrieve files while respecting scoped storage policies on Android 10+.

  • Writing to shared storage

    ContentResolver is used to interact with the shared storage through MediaStore. A ContentValues object stores file metadata like:

    File name (DISPLAY_NAME). File type (MIME_TYPE). Path to save the file (RELATIVE_PATH).

    insert() creates a new file entry in the shared storage, and its Uri is returned. openOutputStream() writes the content to the file, wrapped in a use block to ensure the stream is closed automatically. flush() ensures all data is saved to the file system.


    fun writeTextFile(context: Context, fileName: String, fileContent: String) {
    val resolver = context.contentResolver
    val contentValues = ContentValues().apply {
    put(MediaStore.MediaColumns.DISPLAY_NAME, fileName)
    put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
    put(MediaStore.MediaColumns.RELATIVE_PATH, "Documents/") // Path for shared storage
    }

    val fileUri: Uri? = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
    fileUri?.let {
    resolver.openOutputStream(it)?.use { outputStream ->
    outputStream.write(fileContent.toByteArray())
    outputStream.flush()
    }
    }
    }
  • Reading from shared storage

    ContentResolver queries the MediaStore to find the file by its name. The query returns a Cursor that:

    • Checks if the file exists using moveToFirst().
    • Retrieves the file’s path using the DATA column.

    A File object reads the file using readText() to get its content.


    fun readTextFile(context: Context, fileName: String): String? {
    val resolver = context.contentResolver
    val projection = arrayOf(MediaStore.MediaColumns.DISPLAY_NAME, MediaStore.MediaColumns.DATA)
    val selection = "${MediaStore.MediaColumns.DISPLAY_NAME} = ?"
    val selectionArgs = arrayOf(fileName)
    val uri: Uri = MediaStore.Files.getContentUri("external")

    resolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor ->
    if (cursor.moveToFirst()) {
    val filePath = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA))
    return filePath?.let {
    return File(it).readText()
    }
    }
    }
    return null // Return null if file not found
    }

Preferences

Preferences allow you to store private, primitive data (e.g., String, int, boolean) in key-value pairs using SharedPreferences. Suitable for lightweight data like user settings, flags, or configuration.

  • initializing preferences

    "AppPrefs" is the name of the SharedPreferences file where data will be stored. If a file with this name doesn't exist, Android creates it. Multiple SharedPreferences files can exist; the name ensures you’re accessing the correct one. Context.MODE_PRIVATE defines the access mode for the file. MODE_PRIVATE means the file is accessible only to your app.

      private lateinit var sharedPreferences: SharedPreferences

    override fun onCreate(savedInstanceState: Bundle?) {

    sharedPreferences = getSharedPreferences("AppPrefs", Context.MODE_PRIVATE)

    ...
    }
  • writing to preferences

    fun saveToSharedPreferences(key: String, value: String) {
    with(sharedPreferences.edit()) {
    putString(key, value)
    apply()
    }
    }
    note

    For other value types you can use putInt() putBoolean() etc

  • reading from preferences

    fun readFromSharedPreferences(key: String): String? {
    return sharedPreferences.getString(key, "")
    }
    note

    For other value types you can use getInt() getBoolean() etc

Databases

For structured data, you can use Room, a persistence library that provides an abstraction over SQLite.

There are three major components in Room:

  • The database class that holds the database and serves as the main access point for the underlying connection to your app's persisted data.
    @Database(entities = [User::class], version = 1)
    abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    }
  • Data entities that represent tables in your app's database.
    @Entity
    data class User(
    @PrimaryKey val uid: Int,
    @ColumnInfo(name = "first_name") val firstName: String?,
    @ColumnInfo(name = "last_name") val lastName: String?
    )
  • Data access objects (DAOs) that provide methods that your app can use to query, update, insert, and delete data in the database.
    @Dao
    interface UserDao {
    @Query("SELECT * FROM user")
    fun getAll(): List<User>

    @Query("SELECT * FROM user WHERE uid IN (:userIds)")
    fun loadAllByIds(userIds: IntArray): List<User>

    @Query("SELECT * FROM user WHERE first_name LIKE :first AND " +
    "last_name LIKE :last LIMIT 1")
    fun findByName(first: String, last: String): User

    @Insert
    fun insertAll(vararg users: User)

    @Delete
    fun delete(user: User)
    }

Using the database

val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "database-name"
).build()
val userDao = db.userDao()
val users: List<User> = userDao.getAll()
info

You also need to:

  • Add the required Room dependencies in your build.gradle file:

    plugins {
    ...
    id("com.google.devtools.ksp") version "1.9.24-1.0.20"
    }

    Make sure your kotlin version is '1.9.24'

    dependencies {

    val room_version = "2.6.1"

    ksp("androidx.room:room-compiler:$room_version")
    implementation("androidx.room:room-ktx:$room_version")

    ...
    }

Exercises

Create an app with a MainActivity that does the following:

  1. Usea a sharedPreferences to save a boolean which indicates which theme should be loaded when the app starts (Light or Dark). Use a toggle button to swich between themes.
  2. Create a student table in a room database that has the following columns: Name, Year, MeanGrade. Insert a few entries in the database. Display them in the MainActivity.
  3. Write to the internal app-specific storage a .txt file that contains a list with all the students sorted alphabetically.
  4. Write to the shared storage a .txt file that contains a list with all the students sorted by MeanGrade.
info

Use the Device File Explorer to view the .txt files.