App Architecture: Data Layer - DataStore - Android Developers

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.

Try with Kotlin Multiplatform
Kotlin Multiplatform allows sharing the data layer with other platforms. Learn how to set up and work with DataStore in KMP

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:

  1. A flow that can be used to read data from the DataStore

    val data: Flow<T>
    
  2. 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:

  1. 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.

  2. 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.

  3. Do not mix usages of SingleProcessDataStore and MultiProcessDataStore for the same file. If you intend to access the DataStore from more than one process, you must use MultiProcessDataStore.

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