The Complete Guide to Android Data Storage

举报
shenlan9755 发表于 2026/08/22 15:33:08 2026/08/22
【摘要】 The Complete Guide to Android Data StorageFrom a single key-value pair to a full relational database — every storage option Android offers, when to use each, and how to implement them correctly. T...

The Complete Guide to Android Data Storage

From a single key-value pair to a full relational database — every storage option Android offers, when to use each, and how to implement them correctly.


Table of Contents

  1. Choosing the Right Storage
  2. SharedPreferences — Simple Key-Value
  3. DataStore — The Modern Replacement
  4. Internal Storage — Private Files
  5. External Storage & Scoped Storage
  6. SQLite — Raw Database Access
  7. Room — The Recommended Database
  8. FileProvider — Sharing Files Safely
  9. Security & Encryption
  10. Best Practices Checklist
  11. Conclusion

1. Choosing the Right Storage

Android offers many storage options. Pick based on data complexity, persistence needs, and visibility:

Storage Best For Async? Structured? Visibility
SharedPreferences Simple settings (deprecated path) No No App-private
DataStore (Preferences) Simple settings (modern) Yes (coroutines) No App-private
DataStore (Proto) Typed structured settings Yes Yes (schema) App-private
Internal storage Private files (cache, sensitive) No No App-private
External storage Media, documents, shared files No No Shared (scoped)
SQLite (raw) Relational data (low-level) No Yes App-private
Room Relational data (recommended) Yes Yes App-private
EncryptedSharedPreferences Sensitive small data No No App-private

Golden rule: Use DataStore for key-value settings, Room for structured data, and scoped external storage for user-facing media/documents.


2. SharedPreferences — Simple Key-Value

SharedPreferences is the oldest and simplest API. It’s still everywhere, but Google now recommends DataStore for new code.

2.1 Write

val sharedPrefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)

sharedPrefs.edit().apply {
    putString("username", "alice")
    putInt("age", 28)
    putBoolean("is_premium", true)
    apply()   // async — preferred over commit()
}.also { /* nothing */ }
  • apply() — saves asynchronously (background thread). Use this in 99% of cases.
  • commit() — saves synchronously and returns a Boolean. Use only when you need immediate confirmation.

2.2 Read

val username = sharedPrefs.getString("username", "default_user")
val age = sharedPrefs.getInt("age", 0)
val isPremium = sharedPrefs.getBoolean("is_premium", false)

2.3 Listen for changes

sharedPrefs.registerOnSharedPreferenceChangeListener { prefs, key ->
    when (key) {
        "username" -> println("Username changed to ${prefs.getString(key, "")}")
    }
}

Warning: The listener holds a strong reference. Unregister it in onDestroy() to avoid memory leaks.

2.4 Why migrate away?

  • No type safety — everything is read with explicit casts and defaults.
  • No async API — apply() hides disk I/O but can still block on getString() if called before the write completes.
  • Not designed for large or structured data.
  • Prone to ClassCastException if schema evolves.

3. DataStore — The Modern Replacement

DataStore is Jetpack’s async, type-safe replacement for SharedPreferences. There are two flavors:

  • Preferences DataStore — key-value, no schema (direct replacement for SharedPreferences).
  • Proto DataStore — typed objects backed by Protocol Buffers (schema-safe).

3.1 Preferences DataStore

Add dependency:

dependencies {
    implementation("androidx.datastore:datastore-preferences:1.1.1")
}

Create the DataStore:

private val Context.dataStore by preferencesDataStore(name = "user_prefs")

Define keys:

object UserPrefs {
    val USERNAME = stringPreferencesKey("username")
    val AGE = intPreferencesKey("age")
    val IS_PREMIUM = booleanPreferencesKey("is_premium")
}

Write (suspend):

suspend fun saveUser(username: String, age: Int, isPremium: Boolean) {
    context.dataStore.edit { prefs ->
        prefs[UserPrefs.USERNAME] = username
        prefs[UserPrefs.AGE] = age
        prefs[UserPrefs.IS_PREMIUM] = isPremium
    }
}

Read (Flow):

val usernameFlow: Flow<String> = context.dataStore.data
    .map { it[UserPrefs.USERNAME] ?: "default_user" }

// Collect in a coroutine
viewModelScope.launch {
    usernameFlow.collect { username ->
        // update UI
    }
}

Read once (suspend):

suspend fun getUsername(): String {
    return context.dataStore.data.first()[UserPrefs.USERNAME] ?: "default_user"
}

3.2 Proto DataStore

For structured settings (e.g., a UserSettings object with multiple fields), Proto DataStore gives you a typed schema. It requires a .proto file and generated classes — more setup, but fully type-safe and evolvable.

Recommendation: Start with Preferences DataStore. Move to Proto DataStore only if you have many related fields that belong together as an object.


4. Internal Storage — Private Files

Internal storage is app-private, always available, and automatically deleted when the user uninstalls your app. Perfect for sensitive data and temporary files.

4.1 Write a file

fun writeToFile(filename: String, content: String) {
    context.openFileOutput(filename, Context.MODE_PRIVATE).use { output ->
        output.write(content.toByteArray())
    }
}

4.2 Read a file

fun readFromFile(filename: String): String {
    return context.openFileInput(filename).bufferedReader().use { it.readText() }
}

4.3 Cache directory

For temporary files that the system may evict under low-storage pressure:

val cacheFile = File(context.cacheDir, "temp_image.jpg")

Tip: Call context.cacheDir.deleteRecursively() on low-memory warnings (onTrimMemory) to free space proactively.


5. External Storage & Scoped Storage

Starting with Android 10 (API 29), Google introduced Scoped Storage. Apps can no longer freely read/write the entire shared filesystem. Instead, use dedicated APIs per media type.

5.1 Save a media image (MediaStore)

@RequiresApi(Build.VERSION_CODES.Q)
fun saveImage(bitmap: Bitmap, displayName: String): Uri {
    val contentValues = ContentValues().apply {
        put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
        put(MediaStore.MediaColumns.RELATIVE_PATH, "${Environment.DIRECTORY_PICTURES}/MyApp")
    }

    val uri = context.contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        contentValues
    ) ?: throw IOException("Failed to create MediaStore entry")

    context.contentResolver.openOutputStream(uri)?.use { out ->
        bitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
    }

    return uri
}

5.2 Pick a document (Storage Access Framework)

For user-chosen files (documents, images, any type), use the Storage Access Framework — no permissions needed:

val pickFile = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
    uri?.let {
        // Read the file via contentResolver.openInputStream(it)
    }
}

// Launch with MIME types
pickFile.launch(arrayOf("image/*", "application/pdf"))

5.3 App-specific external storage

For files that are app-private but on shared media (not auto-deleted on uninstall, but cleared via “Clear data”):

// Persistent
val file = File(context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "photo.jpg")

// Cache
val cacheFile = File(context.externalCacheDir, "temp.jpg")

No permission required for app-specific external directories.

5.4 Permissions summary

Action Permission needed
App-specific dirs (getExternalFilesDir) None
Read shared media via MediaStore None (API 29+)
Read all media (broad access) READ_MEDIA_IMAGES / READ_MEDIA_VIDEO / READ_MEDIA_AUDIO (API 33+)
Modify media not created by your app MediaStore.createWriteRequest() + user consent dialog
Legacy broad access (API < 29) READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE

6. SQLite — Raw Database Access

Android ships with SQLite. You can use it directly via SQLiteOpenHelper, but Room is strongly preferred. Here’s the raw approach for completeness.

6.1 Define the helper

class AppDatabase(context: Context) : SQLiteOpenHelper(context, "app.db", null, 1) {

    override fun onCreate(db: SQLiteDatabase) {
        db.execSQL("""
            CREATE TABLE users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                name TEXT NOT NULL,
                email TEXT UNIQUE,
                created_at INTEGER
            )
        """.trimIndent())
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
        db.execSQL("DROP TABLE IF EXISTS users")
        onCreate(db)
    }
}

6.2 Insert & query

val db = AppDatabase(context).writableDatabase

// Insert
val values = ContentValues().apply {
    put("name", "Alice")
    put("email", "alice@example.com")
    put("created_at", System.currentTimeMillis())
}
db.insert("users", null, values)

// Query
val cursor = db.query("users", null, "name = ?", arrayOf("Alice"), null, null, null)
cursor.use {
    while (it.moveToNext()) {
        val id = it.getLong(it.getColumnIndexOrThrow("id"))
        val name = it.getString(it.getColumnIndexOrThrow("name"))
    }
}

Why avoid raw SQLite? No compile-time query validation, manual cursor management, boilerplate mapping, and easy SQL injection if you concatenate strings. Room solves all of this.


7. Room — The Recommended Database

Room is an abstraction layer over SQLite that provides compile-time query validation, automatic mapping, and coroutine/RxJava support.

7.1 Add dependencies

dependencies {
    val roomVersion = "2.6.1"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")
    ksp("androidx.room:room-compiler:$roomVersion")
}

Use ksp (Kotlin Symbol Processing) instead of kapt for faster builds.

7.2 Define the Entity

@Entity(tableName = "users")
data class User(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    @ColumnInfo(name = "name") val name: String,
    @ColumnInfo(name = "email") val email: String,
    @ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis()
)

7.3 Define the DAO

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insert(user: User): Long

    @Update
    suspend fun update(user: User)

    @Delete
    suspend fun delete(user: User)

    @Query("SELECT * FROM users WHERE id = :id")
    suspend fun getById(id: Long): User?

    @Query("SELECT * FROM users ORDER BY name ASC")
    fun getAllFlow(): Flow<List<User>>   // observable query

    @Query("SELECT * FROM users WHERE name LIKE :query")
    suspend fun search(query: String): List<User>
}

7.4 Define the Database

@Database(entities = [User::class], version = 1, exportSchema = true)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao

    companion object {
        @Volatile private var INSTANCE: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                INSTANCE ?: Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app.db"
                )
                .fallbackToDestructiveMigration()
                .build()
                .also { INSTANCE = it }
            }
        }
    }
}

7.5 Use it

class UserRepository(private val dao: UserDao) {
    suspend fun createUser(name: String, email: String): Long {
        return dao.insert(User(name = name, email = email))
    }

    fun observeAllUsers(): Flow<List<User>> = dao.getAllFlow()
}

7.6 Migrations

When you change the schema, increment the version and provide a migration:

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("ALTER TABLE users ADD COLUMN avatar_url TEXT")
    }
}

Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
    .addMigrations(MIGRATION_1_2)
    .build()

Always set exportSchema = true and commit the generated schema JSON files. They enable automated migration testing.


8. FileProvider — Sharing Files Safely

To share a file (e.g., via an intent to another app), you must use a ContentUri via FileProvider. Sharing a raw file:// URI throws FileUriExposedException on API 24+.

8.1 Declare in AndroidManifest.xml

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

8.2 Define paths in res/xml/file_paths.xml

<paths>
    <files-path name="my_files" path="." />
    <cache-path name="my_cache" path="." />
    <external-files-path name="my_external" path="." />
</paths>

8.3 Get the URI and share

fun shareImage(file: File) {
    val uri = FileProvider.getUriForFile(
        context,
        "${context.packageName}.fileprovider",
        file
    )

    val intent = Intent(Intent.ACTION_SEND).apply {
        type = "image/jpeg"
        putExtra(Intent.EXTRA_STREAM, uri)
        addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
    }

    context.startActivity(Intent.createChooser(intent, "Share image"))
}

9. Security & Encryption

9.1 EncryptedSharedPreferences

For sensitive key-value data (tokens, small secrets), use the Jetpack Security library:

implementation("androidx.security:security-crypto:1.1.0-alpha06")
val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val encryptedPrefs = EncryptedSharedPreferences.create(
    context,
    "secret_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

encryptedPrefs.edit().putString("auth_token", token).apply()

9.2 SQLCipher for encrypted databases

For encrypted Room databases, use the sqlcipher support library. It transparently encrypts the entire DB file with a passphrase.

9.3 Android Keystore

For cryptographic keys that must never leave the device, use the Android Keystore:

val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
keyGenerator.init(
    KeyGenParameterSpec.Builder(
        "my_key_alias",
        KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    )
    .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
    .build()
)
val secretKey = keyGenerator.generateKey()

Keys in the Keystore are hardware-backed on devices with a Trusted Execution Environment (TEE).


10. Best Practices Checklist

  • [ ] Use DataStore instead of SharedPreferences for new code.
  • [ ] Use Room instead of raw SQLiteOpenHelper.
  • [ ] Never store secrets in plaintext — use EncryptedSharedPreferences or the Keystore.
  • [ ] Scope file access — prefer MediaStore and SAF over broad READ_EXTERNAL_STORAGE.
  • [ ] Use apply() not commit() for SharedPreferences writes (async).
  • [ ] Unregister listeners (registerOnSharedPreferenceChangeListener) to avoid leaks.
  • [ ] Export Room schemas (exportSchema = true) and write migration tests.
  • [ ] Use @Volatile + double-checked locking for singleton database instances.
  • [ ] Clean cache on onTrimMemory() to be a good system citizen.
  • [ ] Use FileProvider to share files — never raw file:// URIs.
  • [ ] Run DB operations off the main thread — Room enforces this by default.
  • [ ] Use Flow for observable queries so your UI auto-updates on data changes.

11. Conclusion

Android storage has evolved significantly. Here’s the modern stack at a glance:

Need Use
Simple settings Preferences DataStore
Typed settings Proto DataStore
Structured data Room
Private files Internal storage (openFileOutput)
Media/documents MediaStore + Storage Access Framework
Sensitive small data EncryptedSharedPreferences
Cryptographic keys Android Keystore
Sharing files FileProvider

The migration from legacy APIs (SharedPreferences, raw SQLite, broad storage permissions) to modern ones (DataStore, Room, scoped storage) is worth the effort. You get type safety, async APIs, better security defaults, and forward compatibility with future Android versions.

Start with DataStore and Room in your next project — your future self will thank you.


Written by [Your Name] · Last updated: 2026-08-22

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。