DataStore Part of Android Jetpack.
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.
Preferences DataStore and Proto DataStore
DataStore provides two different implementations: Preferences DataStore and Proto DataStore.
- Preferences DataStore stores and accesses data using keys. This implementation does not require a predefined schema, and it does not provide type safety.
- Proto DataStore 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.
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 throwIllegalStateException
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
andMultiProcessDataStore
for the same file. If you intend to access theDataStore
from more than one process, always useMultiProcessDataStore
.
Setup
To use Jetpack DataStore in your app, add the following to your Gradle file depending on which implementation you want to use:
Preferences DataStore
Groovy
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation "androidx.datastore:datastore-preferences:1.1.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-preferences-core:1.1.1" }
Kotlin
// Preferences DataStore (SharedPreferences like APIs) dependencies { implementation("androidx.datastore:datastore-preferences:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-preferences-core:1.1.1") }
Proto DataStore
Groovy
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation "androidx.datastore:datastore:1.1.1" // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.1" } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation "androidx.datastore:datastore-core:1.1.1" }
Kotlin
// Typed DataStore (Typed API surface, such as Proto) dependencies { implementation("androidx.datastore:datastore:1.1.1") // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.1") } // Alternatively - use the following artifact without an Android dependency. dependencies { implementation("androidx.datastore:datastore-core:1.1.1") }
Store key-value pairs with Preferences DataStore
The Preferences DataStore implementation uses the
DataStore
and
Preferences
classes to persist simple key-value pairs to disk.
Create a Preferences DataStore
Use the property delegate created by preferencesDataStore
to create an instance of DataStore<Preferences>
. Call it once at the top level of your kotlin file, and access it through this property throughout the rest of your application. This makes it easier to keep your DataStore
as a singleton. Alternatively, use RxPreferenceDataStoreBuilder
if you're using RxJava. The mandatory name
parameter is the name of the
Preferences DataStore.
Kotlin
// At the top level of your kotlin file: val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
Java
RxDataStore<Preferences> dataStore = new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();
Read from a Preferences DataStore
Because Preferences DataStore does not use a predefined schema, you must use the
corresponding key type function to define a key for each value that you need to
store in the DataStore<Preferences>
instance. For example, to define a key
for an int value, use
intPreferencesKey()
.
Then, use the
DataStore.data
property
to expose the appropriate stored value using a Flow
.
Kotlin
val EXAMPLE_COUNTER = intPreferencesKey("example_counter") val exampleCounterFlow: Flow<Int> = context.dataStore.data .map { preferences -> // No type safety. preferences[EXAMPLE_COUNTER] ?: 0 }
Java
Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter"); Flowable<Integer> exampleCounterFlow = dataStore.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));
Write to a Preferences DataStore
Preferences DataStore provides an
edit()
function that transactionally updates the data in a DataStore
. The function's
transform
parameter accepts a block of code where you can update the values as
needed. All of the code in the transform block is treated as a single
transaction.
Kotlin
suspend fun incrementCounter() { context.dataStore.edit { settings -> val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0 settings[EXAMPLE_COUNTER] = currentCounterValue + 1 } }
Java
Single<Preferences> updateResult = dataStore.updateDataAsync(prefsIn -> { MutablePreferences mutablePreferences = prefsIn.toMutablePreferences(); Integer currentInt = prefsIn.get(INTEGER_KEY); mutablePreferences.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1); return Single.just(mutablePreferences); }); // The update is completed once updateResult is completed.
Store typed objects with Proto DataStore
The Proto DataStore implementation uses DataStore and protocol buffers to persist typed objects to disk.
Define a schema
Proto DataStore requires a predefined schema in a proto file in the
app/src/main/proto/
directory. This schema defines the type for the objects
that you persist in your Proto DataStore. To learn more about defining a proto
schema, see the protobuf language
guide.
syntax = "proto3";
option java_package = "com.example.application";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Create a Proto DataStore
There are two steps involved in creating a Proto DataStore to store your typed objects:
- Define a class that implements
Serializer<T>
, whereT
is the type defined in the proto file. This serializer class tells DataStore how to read and write your data type. Make sure you include a default value for the serializer to be used if there is no file created yet. - Use the property delegate created by
dataStore
to create an instance ofDataStore<T>
, whereT
is the type defined in the proto file. Call this once at the top level of your kotlin file and access it through this property delegate throughout the rest of your app. Thefilename
parameter tells DataStore which file to use to store the data, and theserializer
parameter tells DataStore the name of the serializer class defined in step 1.
Kotlin
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 )
Java
private static class SettingsSerializer implements Serializer<Settings> { @Override public Settings getDefaultValue() { Settings.getDefaultInstance(); } @Override public Settings readFrom(@NotNull InputStream input) { try { return Settings.parseFrom(input); } catch (exception: InvalidProtocolBufferException) { throw CorruptionException(“Cannot read proto.”, exception); } } @Override public void writeTo(Settings t, @NotNull OutputStream output) { t.writeTo(output); } } RxDataStore<Byte> dataStore = new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();
Read from a Proto DataStore
Use DataStore.data
to expose a Flow
of the appropriate property from your stored object.
Kotlin
val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data .map { settings -> // The exampleCounter property is generated from the proto schema. settings.exampleCounter }
Java
Flowable<Integer> exampleCounterFlow = dataStore.data().map(settings -> settings.getExampleCounter());
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.
Kotlin
suspend fun incrementCounter() { context.settingsDataStore.updateData { currentSettings -> currentSettings.toBuilder() .setExampleCounter(currentSettings.exampleCounter + 1) .build() } }
Java
Single<Settings> updateResult = dataStore.updateDataAsync(currentSettings -> Single.just( currentSettings.toBuilder() .setExampleCounter(currentSettings.getExampleCounter() + 1) .build()));
Use DataStore in synchronous code
One of the primary benefits of DataStore is the asynchronous API, but it may not always be feasible to change your surrounding code to be asynchronous. This might be the case if you're working with an existing codebase that uses synchronous disk I/O or if you have a dependency that doesn't provide an asynchronous API.
Kotlin coroutines provide the
runBlocking()
coroutine builder to help bridge the gap between synchronous and asynchronous
code. You can use runBlocking()
to read data from DataStore synchronously.
RxJava offers blocking methods on Flowable
. The following code blocks the calling
thread until DataStore returns data:
Kotlin
val exampleData = runBlocking { context.dataStore.data.first() }
Java
Settings settings = dataStore.data().blockingFirst();
Performing synchronous I/O operations on the UI thread can cause ANRs or UI jank. You can mitigate these issues by asynchronously preloading the data from DataStore:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { lifecycleScope.launch { context.dataStore.data.first() // You should also handle IOExceptions here. } }
Java
dataStore.data().first().subscribe();
This way, DataStore asynchronously reads the data and caches it in memory. Later
synchronous reads using runBlocking()
may be faster or may avoid a disk I/O
operation altogether if the initial read has completed.
Use DataStore in multi-process code
You can configure DataStore to access the same data across different processes with the same data consistency guarantees as from within a single process. In particular, DataStore guarantees:
- Reads only return the data that has been persisted to disk.
- Read-after-write consistency.
- Writes are serialized.
- Reads are never blocked by writes.
Consider a sample application with a service and an activity:
The service is running in a separate process and periodically updates the DataStore
<service android:name=".MyService" android:process=":my_process_id" />
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { scope.launch { while(isActive) { dataStore.updateData { Settings(lastUpdate = System.currentTimeMillis()) } delay(1000) } } }
While the app would collect those changes and update its UI
val settings: Settings by dataStore.data.collectAsState() Text( text = "Last updated: $${settings.timestamp}", )
To be able to use DataStore across different processes, you need to construct
the DataStore object using the MultiProcessDataStoreFactory
.
val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
}
)
serializer
tells DataStore how to read and write your data type.
Make sure you include a default value for the serializer to be used if there is
no file created yet. Below is an example implementation using
kotlinx.serialization:
@Serializable
data class Settings(
val lastUpdate: Long
)
@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {
override val defaultValue = Settings(lastUpdate = 0)
override suspend fun readFrom(input: InputStream): Timer =
try {
Json.decodeFromString(
Settings.serializer(), input.readBytes().decodeToString()
)
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read Settings", serialization)
}
override suspend fun writeTo(t: Settings, output: OutputStream) {
output.write(
Json.encodeToString(Settings.serializer(), t)
.encodeToByteArray()
)
}
}
You can use Hilt dependency injection to make sure that your DataStore instance is unique per process:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Provide feedback
Share your feedback and ideas with us through these resources:
- Issue tracker
- Report issues so we can fix bugs.
Additional resources
To learn more about Jetpack DataStore, see the following additional resources:
Samples
Blogs
Codelabs
Recommended for you
- Note: link text is displayed when JavaScript is off
- Load and display paged data
- LiveData overview
- Layouts and binding expressions