Android Storage System is the name given to these storage systems. Internal storage, external storage, shared preferences, database, and shared storage are some of the storage options offered by Android.
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: (Content provider*) Store files that your app intends to share with other apps, including media, documents, and other files.
Preferences(now Jetpack Datastore): Store private, primitive data in key-value pairs.
Databases: Store structured data in a private database using the Room persistence library.
Scoped storage
To give users more control over their files and to limit file clutter, apps that target Android 10 (API level 29) and higher are given scoped access into external storage, or scoped storage, by default. Such apps have access only to the app-specific directory on external storage, as well as specific types of media that the app has created.
Use shared storage for user data that can or should be accessible to other apps and saved even if the user uninstalls your app.
Android provides APIs for storing and accessing the following types of shareable data:
Media content: The system provides standard public directories for these kinds of files, so the user has a common location for all their photos, another common location for all their music and audio files, and so on. Your app can access this content using the platform's MediaStore API.
Documents and other files: The system has a special directory for containing other file types, such as PDF documents and books that use the EPUB format. Your app can access these files using the platform's Storage Access Framework.
Datasets: On Android 11 (API level 30) and higher, the system caches large datasets that multiple apps might use. These datasets can support use cases like machine learning and media playback. Apps can access these shared datasets using the BlobStoreManager API.
Jetpack DataStore is a data storage solution that allows you to store key-value pairs or typed objects with protocol buffers. DataStore uses Kotlin coroutines and Flow to store data asynchronously, consistently, and transactionally.
If you're currently using SharedPreferences to store data, consider migrating to DataStore instead.
Note: If you need to support large or complex datasets, partial updates, or referential integrity, consider using Room instead of DataStore. DataStore is ideal for small, simple datasets and does not support partial updates or referential integrity.
Preferences DataStore and Proto DataStore
DataStore provides two different implementations: Preferences DataStore and Proto DataStore.
stores and accesses data using keys. This implementation does not require a predefined schema, and it does not provide type safety.
Using DataStore correctly
In order to use DataStore correctly always keep in mind the following rules:
Never create more than one instance of DataStore for a given file in the same process. Doing so can break all DataStore functionality. If there are multiple DataStores active for a given file in the same process, DataStore will throw IllegalStateException when reading or updating data.
The generic type of the DataStore must be immutable. Mutating a type used in DataStore invalidates any guarantees that DataStore provides and creates potentially serious, hard-to-catch bugs. It is strongly recommended that you use protocol buffers which provide immutability guarantees, a simple API and efficient serialization.
Never mix usages of SingleProcessDataStore and MultiProcessDataStore for the same file. If you intend to access the DataStore from more than one process always use MultiProcessDataStore
Store key-value pairs with Preferences DataStore
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Read from a Preferences DataStore
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
.map { preferences ->
// No type safety.
preferences[EXAMPLE_COUNTER] ?: 0
}
Write to a Preferences DataStore
suspend fun incrementCounter() {
context.dataStore.edit { settings ->
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
settings[EXAMPLE_COUNTER] = currentCounterValue + 1
}
}
stores data as instances of a custom data type. This implementation requires you to define a schema using protocol buffers, but it provides type safety.
Store typed objects with Proto DataStore
Define a schema
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Create a Proto DataStore
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings.getDefaultInstance()
override suspend fun readFrom(input: InputStream): Settings {
try {
return Settings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(
t: Settings,
output: OutputStream) = t.writeTo(output)
}
val Context.settingsDataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer
)
Read from a Proto DataStore
Use DataStore.data to expose a Flow of the appropriate property from your stored object.
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
.map { settings ->
// The exampleCounter property is generated from the proto schema.
settings.exampleCounter
}
Write to a Proto DataStore
Proto DataStore provides an updateData() function that transactionally updates a stored object. updateData() gives you the current state of the data as an instance of your data type and updates the data transactionally in an atomic read-write-modify operation
suspend fun incrementCounter() {
context.settingsDataStore.updateData { currentSettings ->
currentSettings.toBuilder()
.setExampleCounter(currentSettings.exampleCounter + 1)
.build()
}
}
The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. In particular, Room provides the following benefits:
Compile-time verification of SQL queries.
Convenience annotations that minimize repetitive and error-prone boilerplate code.
Streamlined database migration paths.
Because of these considerations, we highly recommend that you use Room instead of using the SQLite APIs directly.
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.
Data entities that represent tables in your app's database.
Data access objects (DAOs) that provide methods that your app can use to query, update, insert, and delete data in the database.
The database class provides your app with instances of the DAOs associated with that database. In turn, the app can use the DAOs to retrieve data from the database as instances of the associated data entity objects. The app can also use the defined data entities to update rows from the corresponding tables, or to create new rows for insertion. Figure 1 illustrates the relationship between the different components of Room.
The following code defines an AppDatabase class to hold the database. AppDatabase defines the database configuration and serves as the app's main access point to the persisted data. The database class must satisfy the following conditions:
The class must be annotated with a @Database annotation that includes an entities array that lists all of the data entities associated with the database.
The class must be an abstract class that extends RoomDatabase.
For each DAO class that is associated with the database, the database class must define an abstract method that has zero arguments and returns an instance of the DAO class.
As you add and change features in your app, you need to modify your Room entity classes and underlying database tables to reflect these changes. It's important to preserve user data that is already in the on-device database when an app update changes the database schema.
Room supports both automated and manual options for incremental migration. Automatic migrations work for most basic schema changes, but you might need to manually define migration paths for more complex changes.
To declare an automated migration between two database versions, add an @AutoMigration annotation to the autoMigrations property in @Database:
To use the AutoMigrationSpec implementation for an automated migration, set the spec property in the corresponding @AutoMigration annotation:
You can use AutoMigrationSpec to give Room the additional information that it needs to correctly generate migration paths. Define a static class that implements AutoMigrationSpec in your RoomDatabase class and annotate it with one or more of the following:
If your app needs to do more work after the automated migration completes, you can implement onPostMigrate(). If you implement this method in your AutoMigrationSpec, Room calls it after the automated migration completes.
In cases where a migration involves complex schema changes, Room might not be able to generate an appropriate migration path automatically. For example, if you decide to split the data in a table into two tables, Room can't tell how to perform this split. In cases like these, you must manually define a migration path by implementing a Migration class.
A Migration class explicitly defines a migration path between a startVersion and an endVersion by overriding the Migration.migrate() method. Add your Migration classes to your database builder using the addMigrations() method: