Project: /architecture/_project.yaml Book: /architecture/_book.yaml keywords: datastore, architecture, api:JetpackDataStore description: Explore this app architecture guide on data layer libraries to learn about Preferences DataStore and Proto DataStore, Setup, and more. hide_page_heading: true
DataStore Part of Android Jetpack.
Jetpack DataStore is a data storage solution that lets you 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 using SharedPreferences to store data, consider migrating to
DataStore instead.
DataStore API
The DataStore interface provides the following API:
A flow that can be used to read data from the DataStore
val data: Flow<T>A function to update data in the DataStore
suspend updateData(transform: suspend (t) -> T)
DataStore Configurations
If you want to store and accesses data using keys, use the Preferences
DataStore implementation which does not require a predefined schema, and it does
not provide type safety. It has a SharedPreferences-like API but doesn't
have the drawbacks associated with shared preferences.
DataStore lets you persist custom classes. To do this, you must define a
schema for the data and provide a Serializer to convert it into a persistable
format. You can choose to use Protocol Buffers, JSON or any other serialization
strategy.
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
Add the following lines to the dependencies part of your gradle file:
Groovy
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation "androidx.datastore:datastore-preferences:1.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.1.7" }
Kotlin
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.1.7") }
To add optional RxJava support, add the following dependencies:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.7" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.7") }
DataStore
Add the following lines to the dependencies part of your gradle file:
Groovy
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation "androidx.datastore:datastore:1.1.7" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.1.7" }
Kotlin
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.1.7") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.1.7") }
Add the following optional dependencies for RxJava support:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.1.7" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.1.7" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.1.7") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.1.7") }
To serialize content, add dependencies for either Protocol Buffers or JSON serialization.
JSON serialization
To use JSON serialization, add the following to your Gradle file:
Groovy
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" }
Kotlin
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") }
Protobuf serialization
To use Protobuf serialization, add the following to your Gradle file:
Groovy
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1" } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
Kotlin
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1") } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
Use DataStore correctly
In order to use DataStore correctly always keep in mind the following rules:
Never create more than one instance of
DataStorefor 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 throwIllegalStateExceptionwhen reading or updating data.The generic type of the
DataStore<T>must be immutable. Mutating a type used in DataStore invalidates the consistency that DataStore provides and creates potentially serious, hard-to-catch bugs. We recommend that you use protocol buffers, which help ensure immutability, a clear API, and efficient serialization.Do not mix usages of
SingleProcessDataStoreandMultiProcessDataStorefor the same file. If you intend to access theDataStorefrom more than one process, you must useMultiProcessDataStore.
Data Definition
Preferences DataStore
Define a key that will be used to persist data to disk.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
JSON DataStore
For JSON datastore, add a @Serialization annotation to the data that you
want to persist
@Serializable
data class Settings(
val exampleCounter: Int
)
Define a class that implements Serializer<T>, where T is the type of the
class you added the earlier annotation to. Make sure you include a default
value for the serializer to be used if there is no file created yet.
object SettingsSerializer : Serializer<Settings> {
override val defaultValue: Settings = Settings(exampleCounter = 0)
override suspend fun readFrom(input: InputStream): Settings =
try {
Json.decodeFromString<Settings>(
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(t)
.encodeToByteArray()
)
}
}
Proto DataStore
The Proto DataStore implementation uses DataStore and protocol buffers to persist typed objects to disk.
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.
Add a file called settings.proto inside src/main/proto folder:
syntax = "proto3";
option java_package = "com.example.datastore.snippets.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
Define a class that implements Serializer<T>, where T is the type defined
in the proto file. This serializer class defines how DataStore reads and
writes your data type. Make sure you include a default value for the
serializer to be used if there is no file created yet.
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) {
return t.writeTo(output)
}
}
Create a DataStore
You need to specify a name for the file that is used to persist the data.
Preferences DataStore
The Preferences DataStore implementation uses the DataStore and
Preferences classes to persist key-value pairs to disk. 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. Access DataStore 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.
// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
JSON DataStore
Use the property delegate created by dataStore to create an instance of
DataStore<T>, where T is the serializable data class. Call it once
at the top level of your kotlin file and access it through this property
delegate throughout the rest of your app. The fileName parameter tells
DataStore which file to use to store the data, and the serializer parameter
tells DataStore the name of the serializer class defined in step 1.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.json",
serializer = SettingsSerializer,
)
Proto DataStore
Use the property delegate created by dataStore to create an instance of
DataStore<T>, where T is the type defined in the proto file. Call it
once at the top level of your Kotlin file and access it through this property
delegate throughout the rest of your app. The fileName parameter tells
DataStore which file to use to store the data, and the serializer parameter
tells DataStore the name of the serializer class defined in step 1.
val Context.dataStore: DataStore<Settings> by dataStore(
fileName = "settings.pb",
serializer = SettingsSerializer,
)
Read from DataStore
You need to specify a name for the file that is used to persist the data.
Preferences DataStore
Because Preferences DataStore doesn't 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.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences ->
preferences[EXAMPLE_COUNTER] ?: 0
}
JSON DataStore
Use DataStore.data to expose a Flow of the appropriate property from your
stored object.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
Proto DataStore
Use DataStore.data to expose a Flow of the appropriate property from your
stored object.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings ->
settings.exampleCounter
}
Write to DataStore
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. All of the code in the updateData block is
treated as a single transaction.
Preferences DataStore
suspend fun incrementCounter() {
context.dataStore.updateData {
it.toMutablePreferences().also { preferences ->
preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1
}
}
}
JSON DataStore
suspend fun incrementCounter() {
context.dataStore.updateData { settings ->
settings.copy(exampleCounter = settings.exampleCounter + 1)
}
}
Proto DataStore
suspend fun incrementCounter() {
context.dataStore.updateData { settings ->
settings.copy { exampleCounter = exampleCounter + 1 }
}
}
Compose Sample
You can put these functions together in a class and use it in a Compose app.
Preferences DataStore
We can now put these functions into a class called PreferencesDataStore and
use it in a Compose App.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val preferencesDataStore = remember(context) { PreferencesDataStore(context) }
// Display counter value.
val exampleCounter by preferencesDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(
onClick = {
coroutineScope.launch { preferencesDataStore.incrementCounter() }
}
) {
Text("increment")
}
JSON DataStore
We can now put these functions into a class called JSONDataStore and use it
in a Compose App.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val jsonDataStore = remember(context) { JsonDataStore(context) }
// Display counter value.
val exampleCounter by jsonDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(onClick = { coroutineScope.launch { jsonDataStore.incrementCounter() } }) {
Text("increment")
}
Proto DataStore
We can now put these functions into a class called ProtoDataStore and use it
in a Compose App.
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val protoDataStore = remember(context) { ProtoDataStore(context) }
// Display counter value.
val exampleCounter by protoDataStore.counterFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Counter $exampleCounter",
fontSize = 25.sp
)
// Update the counter.
Button(onClick = { coroutineScope.launch { protoDataStore.incrementCounter() } }) {
Text("increment")
}
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 unresponsive UI. 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 properties as from within a single process. In particular, DataStore provides:
- 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 where the service is running in a separate process and periodically updates the DataStore.
This example uses a JSON datastore, but you can also use a preferences or proto datastore.
@Serializable
data class Time(
val lastUpdateMillis: Long
)
A 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. The following is an example implementation using
kotlinx.serialization:
object TimeSerializer : Serializer<Time> {
override val defaultValue: Time = Time(lastUpdateMillis = 0L)
override suspend fun readFrom(input: InputStream): Time =
try {
Json.decodeFromString<Time>(
input.readBytes().decodeToString()
)
} catch (serialization: SerializationException) {
throw CorruptionException("Unable to read Time", serialization)
}
override suspend fun writeTo(t: Time, output: OutputStream) {
output.write(
Json.encodeToString(t)
.encodeToByteArray()
)
}
}
To be able to use DataStore across different processes, you need to construct
the DataStore object using the MultiProcessDataStoreFactory for both the app
and the service code:
val dataStore = MultiProcessDataStoreFactory.create(
serializer = TimeSerializer,
produceFile = {
File("${context.cacheDir.path}/time.pb")
},
corruptionHandler = null
)
Add the following to your AndroidManifiest.xml:
<service
android:name=".TimestampUpdateService"
android:process=":my_process_id" />
The service periodically calls updateLastUpdateTime(), which writes to the
datastore using updateData.
suspend fun updateLastUpdateTime() {
dataStore.updateData { time ->
time.copy(lastUpdateMillis = System.currentTimeMillis())
}
}
The app reads the value written by the service using the data flow:
fun timeFlow(): Flow<Long> = dataStore.data.map { time ->
time.lastUpdateMillis
}
Now, we can put all these functions together in a class called
MultiProcessDataStore and use it in an App.
Here is the service code:
class TimestampUpdateService : Service() {
val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) }
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
serviceScope.launch {
while (true) {
multiProcessDataStore.updateLastUpdateTime()
delay(1000)
}
}
return START_NOT_STICKY
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
}
And the app code:
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) }
// Display time written by other process.
val lastUpdateTime by multiProcessDataStore.timeFlow()
.collectAsState(initial = 0, coroutineScope.coroutineContext)
Text(
text = "Last updated: $lastUpdateTime",
fontSize = 25.sp
)
DisposableEffect(context) {
val serviceIntent = Intent(context, TimestampUpdateService::class.java)
context.startService(serviceIntent)
onDispose {
context.stopService(serviceIntent)
}
}
You can use Hilt dependency injection so that your DataStore instance is unique per process:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Handle file corruption
There are rare occasions where DataStore's persistent on-disk file could get
corrupted. By default, DataStore doesn't automatically recover from corruption,
and attempts to read from it will cause the system to throw a
CorruptionException.
DataStore offers a corruption handler API that can help you recover gracefully in such a scenario, and avoid throwing the exception. When configured, the corruption handler replaces the corrupted file with a new one containing a predefined default value.
To set up this handler, provide a corruptionHandler when creating the
DataStore instance in by dataStore() or in the DataStoreFactory factory
method:
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.cacheDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
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